7주차 미션 안드로이드 2조 이준혁#24
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 개요Android 애플리케이션에 의존성 주입(DI), 저장소 패턴, UI 상태 기반 아키텍처를 도입하면서 ViewModel을 리팩토링합니다. 제네릭 PostViewModel을 특화된 ViewModel들로 분리하고, 네트워크 계층에 이미지 업로드 기능을 추가하며, 콜백 기반 흐름을 상태 기반 렌더링으로 전환합니다. 변경 사항
시퀀스 다이어그램sequenceDiagram
participant App as App (초기화)
participant AC as AppContainer
participant VM as ViewModel
participant Repo as PostRepository
participant API as ApiService
participant UI as Screen (UI)
App->>AC: AppContainer() 생성
AC->>Repo: PostRepository 인스턴스 생성
UI->>VM: viewModel(factory) 호출
VM->>AC: container.postRepository 접근
VM->>Repo: 저장소 메서드 호출
Repo->>API: API 엔드포인트 호출
API-->>Repo: 응답 반환
Repo-->>VM: Result<T> 반환
VM->>VM: uiState 업데이트 (Loading/Success/Error)
VM-->>UI: StateFlow 구독 업데이트
UI->>UI: 상태 기반 렌더링
예상 코드 리뷰 소요 시간🎯 4 (복잡함) | ⏱️ ~60분 세부 주의 사항:
관련 PR
시
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@coderabbitai review |
|
@alswlekk: I'll review the changes in this pull request right away. ✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (21)
app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt (1)
8-8: 사용하지 않는 import 정리 필요
BuildConfig를 더 이상 사용하지 않으므로 import를 제거해주세요.-import com.example.kuit6_android_api.BuildConfigapp/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt (1)
8-16: 수동 DI Container 구현이 잘 되었습니다Lazy 초기화를 사용한 간단하고 효과적인 DI 구조입니다.
apiService를 private으로 캡슐화하고postRepository만 public으로 노출한 것이 좋습니다.참고: 테스트 용이성을 높이려면
AppContainer를 인터페이스로 만들 수 있습니다:interface AppContainer { val postRepository: PostRepository } class AppContainerImpl : AppContainer { private val apiService: ApiService by lazy { RetrofitClient.apiService } override val postRepository: PostRepository by lazy { PostRepositoryImpl(apiService) } }이렇게 하면 테스트에서 mock container를 주입하기 쉬워집니다. 하지만 현재 구조도 충분히 동작합니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/state/ImageUiState.kt (1)
7-9: 이미지 업로드 응답 타입 개선 권장검증 결과 API가 의도적으로
Map<String, String>을 반환하고 있습니다. UI 레이어에서는imgUrl["imageUrl"]로 접근하고 있으며, 현재 동작은 정상입니다.다만 다음과 같은 이유로 타입 안전성과 명확성 개선을 권장합니다:
- Map 사용으로 의도된 키 구조가 명확하지 않음
- 타입 안전성 부족으로 런타임 오류 가능성
- 다른 응답 DTO들(PostResponse, AuthorResponse)은 data class 사용
다음과 같이 리팩토링하면 코드 품질 향상:
data class ImageUploadResponse( @SerialName("imageUrl") val imageUrl: String ) data class Success( val imageUploadResponse: ImageUploadResponse ) : ImageUiState()변경 필요 위치:
PostRepository.kt- 반환 타입을Result<ImageUploadResponse>로 변경PostRepositoryImpl.kt- uploadImage 함수 구현 수정PostCreateScreen.kt(라인 99),PostEditScreen.kt(라인 103) - imgUrl 접근 로직 수정app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt (1)
30-34: 에러 메시지를 개선하세요."error" 라는 fallback 메시지는 사용자에게 유용한 정보를 제공하지 못합니다. 좀 더 구체적인 에러 메시지나 문자열 리소스를 사용하는 것이 좋습니다.
다음과 같이 개선할 수 있습니다:
.onFailure { error -> _uiState.value = PostListUiState.Error( - message = error.message ?: "error" + message = error.message ?: "게시글을 불러오는 중 오류가 발생했습니다" ) }또는 string resource를 사용하는 방법도 고려해보세요.
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt (3)
51-54: 로딩 인디케이터를 중앙에 배치하세요.
CircularProgressIndicator가 화면 중앙에 배치되지 않아 UX가 좋지 않습니다.다음과 같이 개선하세요:
when (uiState){ is PostListUiState.Loading -> { - CircularProgressIndicator() + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } }
55-70: 타입 캐스팅을 개선하세요.Line 64의
(uiState as PostListUiState.Success)는 안전하지 않은 캐스팅입니다. smart cast를 활용하는 것이 더 안전합니다.다음과 같이 개선하세요:
is PostListUiState.Success -> { + val successState = uiState as PostListUiState.Success LazyColumn( modifier = Modifier .fillMaxSize() .padding(paddingValues) .background(MaterialTheme.colorScheme.background), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - items((uiState as PostListUiState.Success).posts) { post -> + items(successState.posts) { post -> PostItem( post = post, onClick = { onPostClick(post.id) } ) } } }
72-74: 문자열 리소스를 사용하세요.하드코딩된 에러 메시지는 다국어 지원과 유지보수에 좋지 않습니다. 문자열 리소스를 사용하는 것이 좋습니다.
strings.xml에 문자열을 정의하고stringResource()를 사용하세요:Text(stringResource(R.string.loading_failed))app/src/main/java/com/example/kuit6_android_api/ui/post/util/UrlUtil.kt (3)
10-26: 로깅과 스레드 안전성을 개선하세요.두 가지 개선이 필요합니다:
- Line 23의
printStackTrace()는 프로덕션 코드에 적합하지 않습니다. 적절한 로깅 프레임워크(Timber, Log 등)를 사용하세요.- 파일 I/O 작업이 메인 스레드에서 호출될 경우 ANR을 유발할 수 있습니다. 백그라운드 스레드에서 호출해야 함을 문서화하거나, 함수 내부에서 처리하는 것을 고려하세요.
로깅 개선:
} catch (e: Exception) { - e.printStackTrace() + Log.e("UriUtil", "Failed to convert URI to file", e) null }KDoc 추가:
/** * URI를 File로 변환합니다. * * @param context Android Context * @param uri 변환할 URI * @return 변환된 File 또는 실패 시 null * @note 이 함수는 I/O 작업을 수행하므로 백그라운드 스레드에서 호출해야 합니다. */ fun uriToFile(context: Context, uri: Uri): File? {
28-40: 쿼리 효율성을 개선하세요.Line 30에서
contentResolver.query()를 모든 파라미터를 null로 호출하면 모든 컬럼을 반환합니다. 필요한 컬럼만 지정하면 더 효율적입니다.fun getFileName(context: Context, uri: Uri): String? { var fileName: String? = null - val cursor = context.contentResolver.query(uri, null, null, null, null) + val cursor = context.contentResolver.query( + uri, + arrayOf(OpenableColumns.DISPLAY_NAME), + null, + null, + null + ) cursor?.use { if (it.moveToFirst()) { val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (nameIndex != -1) { fileName = it.getString(nameIndex) } } } return fileName }
10-26: 임시 파일 정리 전략을 고려하세요.
context.cacheDir에 생성된 임시 파일들이 누적될 수 있습니다. Android가 자동으로 캐시를 정리하긴 하지만, 명시적인 정리 로직을 추가하는 것이 좋습니다.다음과 같은 방법을 고려해보세요:
- 이미지 업로드 성공 후 임시 파일 삭제
- 앱 시작 시 오래된 캐시 파일 정리
- WorkManager를 사용한 주기적 캐시 정리
예시:
// 업로드 성공 후 tempFile.delete() // 또는 앱 시작 시 fun cleanOldCacheFiles(context: Context) { context.cacheDir.listFiles()?.forEach { file -> if (file.name.startsWith("image_") && System.currentTimeMillis() - file.lastModified() > 24 * 60 * 60 * 1000) { file.delete() } } }app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt (2)
12-24: 에러 로그에 throwable 전체를 넘기는 것이 디버깅에 더 유리합니다.현재
onFailure에서error.message.toString()만 로그로 남겨 스택 트레이스를 잃습니다.Log.e("PostRepository", "...", error)처럼 throwable을 함께 넘기면 원인 추적이 쉬워집니다. 이 패턴은 다른 메서드의onFailure에도 동일하게 적용하면 좋겠습니다.- }.onFailure { error-> - Log.e("PostRepository",error.message.toString()) - } + }.onFailure { throwable -> + Log.e("PostRepository", "getPosts 실패", throwable) + }
12-96: CRUD 메서드의runCatching+ 응답 검사 로직을 공통 헬퍼로 추출하면 중복을 줄일 수 있습니다.모든 메서드가 동일한 패턴(호출 →
success && data != null검사 → 예외 throw → 로그)을 반복합니다. 제네릭 헬퍼 함수를 하나 두고apiService호출과 기본 에러 메시지만 넘기도록 정리하면, 이후 응답 스키마 변경 시 수정 범위를 줄일 수 있습니다.app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt (1)
7-16: 인터페이스는 명확하지만uploadImage응답을 전용 모델로 두면 더 타입 안전합니다.
uploadImage가Result<Map<String, String>>를 반환하면 키 이름 오타나 스키마 변경 시 문제가 런타임까지 미뤄질 수 있습니다. 서버 응답 스키마가 고정돼 있다면ImageUploadResult같은 데이터 클래스를 정의해 반환 타입을 구체화하면 가독성과 안전성이 좋아집니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt (1)
16-21: 삭제 관련 UI 상태의 초기값을Loading대신 “아직 작업 없음”을 나타내도록 바꾸는 것을 고려해 보세요.
_deleteUiState가 초기부터DeletePostUiState.Loading으로 설정돼 있어, 화면 진입만 해도 삭제와 관련된 로딩 UI가 노출될 수 있습니다. 삭제 요청 전에는 “Idle/None” 성격의 상태를 두고, 실제deletePost호출 시에만Loading으로 전환하면 UI 의미가 더 명확해집니다.Also applies to: 38-52
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt (1)
15-77: 편집/이미지 업로드 로직은 명확하지만 다른 ViewModel과의 중복을 줄일 수 있습니다.
uploadImage·clearUploadedImageUrl구현이PostCreateViewModel과 거의 동일해 보여, 향후에는 공통 헬퍼 함수나 베이스 ViewModel로 공유하면 중복 제거와 변경 용이성이 좋아질 것 같습니다. 현재 상태도 동작 상 문제는 없어 보입니다.app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt (1)
70-72: Post 생성 결과를 UiState에 맞춰 처리하고 onPostCreated 호출 위치를 재검토하는 것이 좋습니다현재
uiState를collectAsState()로 구독하지만(Line [70]) 실제로는 어디에서도 사용하지 않고, 버튼 클릭 시viewModel.createPost(...)직후 바로onPostCreated()를 호출하고 있습니다(Line [288]~[295]). 이 구조에서는 네트워크 오류나 서버 에러가 발생해도 화면이 곧바로 닫혀 생성 실패를 사용자가 알아채기 어렵고,PostCreateUiState설계 의도와도 잘 맞지 않을 수 있습니다.가능하다면
PostCreateUiState에 성공/실패 상태를 두고,LaunchedEffect(uiState)에서 성공 상태일 때만onPostCreated()를 호출하며, 에러 상태에서는 Toast 또는 에러 UI를 노출하는 쪽으로 옮기는 것을 권장합니다. 해당 리팩터링 전에 일시적으로라도uiState변수를 사용하지 않는다면 제거해서 불필요한 경고를 줄일 수 있습니다.Also applies to: 285-295
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt (4)
65-67: LaunchedEffect 키를 postId로 두면 재사용 시 더 안전합니다현재 상세 로딩은
LaunchedEffect(Unit)안에서viewModel.loadDetail(postId)를 한 번만 호출합니다(Line [65]~[67]). 만약 동일 Composable 인스턴스를 재사용하면서postId만 바뀌는 네비게이션 패턴을 사용한다면, 새 ID로 이동해도 로딩이 다시 수행되지 않을 수 있습니다. 아래처럼 키를postId로 두는 편이 안전합니다.- LaunchedEffect(Unit) { - viewModel.loadDetail(postId) - } + LaunchedEffect(postId) { + viewModel.loadDetail(postId) + }
103-107: 로딩 상태에서 아무 UI도 표시되지 않아 사용자 경험이 애매할 수 있습니다
when (uiState)의PostDetailUiState.Loading분기가 비어 있어(Line [103]~[107]), 데이터 로딩 중에는 화면이 그냥 비어 보일 가능성이 있습니다. 최소한 중앙에CircularProgressIndicator나 "로딩 중..." 텍스트 정도를 표시해 주면 현재 상태를 사용자에게 명확히 전달할 수 있습니다.
108-203: Success 분기에서 uiState 캐스팅을 반복하기보다는 지역 변수로 한 번만 꺼내는 편이 좋습니다
PostDetailUiState.Success분기 안에서(uiState as PostDetailUiState.Success)캐스팅을 여러 번 반복해서 사용하고 있습니다(Line [127], [129], [155], [162], [170], [185], [195] 등). 다음처럼 한 번만 꺼낸 뒤post를 사용하는 쪽이 가독성과 유지보수 측면에서 더 좋습니다.- is PostDetailUiState.Success -> { - Column( + is PostDetailUiState.Success -> { + val post = (uiState as PostDetailUiState.Success).post + + Column( @@ - if ((uiState as PostDetailUiState.Success).post.author.profileImageUrl != null) { + if (post.author.profileImageUrl != null) { @@ - model = (uiState as PostDetailUiState.Success).post.author.profileImageUrl, + model = post.author.profileImageUrl, @@ - text = (uiState as PostDetailUiState.Success).post.author.username, + text = post.author.username, @@ - text = formatDateTime((uiState as PostDetailUiState.Success).post.createdAt), + text = formatDateTime(post.createdAt), @@ - (uiState as PostDetailUiState.Success).post.imageUrl?.let { imageUrl -> + post.imageUrl?.let { imageUrl -> @@ - text = (uiState as PostDetailUiState.Success).post.title, + text = post.title, @@ - text = (uiState as PostDetailUiState.Success).post.content, + text = post.content,이렇게 정리하면 캐스팅 중복이 사라지고, 후속 필드 추가/변경 시에도 수정 범위가 줄어듭니다.
69-80: 삭제 실패 시 사용자에게도 피드백을 주는 것이 좋습니다삭제 결과를 처리하는
LaunchedEffect(deleteUiState)에서DeletePostUiState.Error인 경우에는 다이얼로그를 닫고Log.e만 호출합니다(Line [75]~[78]). 이 경우 사용자 입장에서는 삭제 시도를 했지만 아무 변화도 없는 것처럼 보일 수 있어 혼란스럽습니다.삭제 실패 시에는 Toast나 Snackbar 등으로 "삭제에 실패했습니다" 같은 메시지를 보여주거나, 최소한 다이얼로그를 유지한 채 에러 메시지를 표시하는 UX를 고려해 보시면 좋겠습니다.
Also applies to: 214-231
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt (1)
70-72: 수정 완료 네비게이션을 edit 결과 UiState와 연동하는 것을 고려해 보세요
uiState와imgUiState를collectAsState()로 구독하고 있지만(Line [70][72]), 수정 버튼 클릭 시에는[253]). 이 경우 실제 수정 API가 실패해도 곧바로 뒤로 이동하거나 리스트를 갱신하는 등 "성공한 것처럼" 보이는 UX가 될 수 있습니다.viewModel.editPost(postId, request)호출 직후 곧바로onPostUpdated()를 실행합니다(Line [245]
PostEditUiState에 성공/에러 상태가 정의되어 있다면, 버튼에서는editPost만 호출하고, 별도의LaunchedEffect(uiState)에서 성공 상태일 때만onPostUpdated()를 실행하며 에러 상태에는 Toast/에러 문구를 표시하는 구조로 옮기는 것을 추천합니다. 이렇게 하면 네트워크/서버 오류에 대한 사용자 피드백과 화면 전환 타이밍을 더 명확하게 제어할 수 있습니다.Also applies to: 242-253
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (29)
.idea/codeStyles/Project.xml(1 hunks).idea/codeStyles/codeStyleConfig.xml(1 hunks)app/src/main/AndroidManifest.xml(1 hunks)app/src/main/java/com/example/kuit6_android_api/App.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/MainActivity.kt(0 hunks)app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt(2 hunks)app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt(6 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt(6 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt(4 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt(3 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt(3 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/DeletePostUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/ImageUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostUIState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/util/UrlUtil.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt(0 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt(1 hunks)
💤 Files with no reviewable changes (2)
- app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
- app/src/main/java/com/example/kuit6_android_api/MainActivity.kt
🔇 Additional comments (12)
.idea/codeStyles/codeStyleConfig.xml (1)
1-5: IDE 설정 파일은 일반적으로 VCS에서 제외됩니다.
.idea디렉토리의 파일들은 로컬 IDE 구성이므로 많은 Android 프로젝트에서.gitignore에 추가됩니다. 이 파일들이 리포지토리에 커밋되어야 하는지 프로젝트 정책을 확인해주세요. 팀 전체가 동일한 코드 스타일을 유지해야 한다면, 이 설정들을 공유하는 것이 도움이 될 수 있습니다..idea/codeStyles/Project.xml (1)
1-157: 코드 스타일 구성이 Android 모범 사례를 따릅니다.IDE 코드 스타일 설정이 잘 구성되어 있습니다:
- Java import 순서(라인 6-34): 표준 Android 규칙에 따라 패키지별로 정리됨
- Kotlin: KOTLIN_OFFICIAL 기본값 사용 (표준)
- XML 정렬(라인 41-153): android 네임스페이스 속성 우선 순위 지정, Android 레이아웃 파일의 모범 사례 준수
이 파일들이 팀 전체의 일관된 코드 스타일을 강제하려는 의도라면 리포지토리에 포함하는 것이 적절할 수 있습니다. 다만
.idea디렉토리 포함 정책을 확인해주세요.app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt (1)
1-15: UI State 구조 잘 구현되었습니다표준적인 sealed class 패턴으로 Loading, Success, Error 상태를 명확하게 표현하고 있습니다.
참고: 여러 UI State 클래스들(PostEditUiState, PostCreateUiState, DeletePostUiState)이 유사한 구조를 가지고 있는데, 필요시 제네릭 UiState를 고려할 수 있습니다. 하지만 현재처럼 각 화면별로 명시적인 타입을 사용하는 것도 타입 안정성과 가독성 측면에서 좋은 접근입니다.
app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt (1)
43-47: 이미지 업로드 엔드포인트 구현 확인Multipart 파일 업로드가 올바르게 구성되었습니다. 백엔드 API 명세를 확인하여 다음 사항이 일치하는지 검증해주세요:
- 엔드포인트 경로:
/api/images/upload- 파라미터 이름:
file(현재 @part에 명시적 이름이 없어 변수명 "file"을 사용)- 응답 형식:
Map<String, String>(어떤 키들이 포함되는지 문서화 권장)필요시 @part 어노테이션에 명시적 이름을 지정할 수 있습니다:
@Part("file") file: MultipartBody.Partapp/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt (1)
1-16: LGTM!게시글 생성을 위한 UI State가 명확하게 정의되었습니다. Loading, Success, Error 상태를 적절히 표현하고 있습니다.
app/src/main/AndroidManifest.xml (1)
17-17: Application 클래스 등록 검증 완료App 클래스가 올바르게 구현되어 있으며,
AppContainer를onCreate()에서 정상적으로 초기화하고 있습니다. AndroidManifest.xml의android:name=".App"등록은 적절합니다.app/src/main/java/com/example/kuit6_android_api/ui/post/state/DeletePostUiState.kt (1)
6-16: 원본 리뷰 의견이 API 계약과 맞지 않습니다코드 검증 결과,
deletePostAPI는 실제로BaseResponse<PostResponse>를 반환하도록 정의되어 있습니다:
ApiService.kt:suspend fun deletePost(@Path id: Long): BaseResponse<PostResponse>PostRepository.kt:suspend fun deletePost(postId: Long): Result<PostResponse>PostRepositoryImpl.kt: 동일하게Result<PostResponse>반환따라서
DeletePostUiState의Success상태에서PostResponse를 저장하는 것은 실제 API 계약과 완벽하게 일치하는 올바른 구현입니다. 변경이 필요 없습니다.Likely an incorrect or invalid review comment.
app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.kt (1)
5-15: 잘 구현되었습니다!UI 상태를 나타내는 sealed class가 올바르게 구현되었으며, 프로젝트의 다른 UI 상태 클래스들과 일관된 패턴을 따르고 있습니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt (1)
5-15: 잘 구현되었습니다!PostList UI 상태가 올바르게 정의되었으며, 프로젝트의 UI 상태 패턴과 일관성을 유지하고 있습니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt (1)
18-20: 초기화 로직이 올바르게 구현되었습니다.
init블록에서loadPosts()를 호출하여 ViewModel 생성 시 데이터를 자동으로 로드하는 패턴이 적절합니다.app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (1)
36-36: ViewModel 팩토리 패턴이 올바르게 적용되었습니다.각 화면별로 적절한 ViewModel을 팩토리를 통해 주입하고 있으며, 일관된 패턴을 유지하고 있습니다.
Also applies to: 51-55, 70-74, 92-96
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt (1)
15-65: PostCreateViewModel의 상태 관리와 책임 분리가 명확합니다.생성 요청과 이미지 업로드를 각각 별도의
StateFlow로 분리하고,Result의onSuccess/onFailure로 UI 상태를 갱신하는 패턴이 깔끔합니다. 현재 구현으로도 읽기 쉽고 유지보수하기 좋아 보입니다.
| class App: Application() { | ||
| lateinit var container: AppContainer | ||
|
|
||
| override fun onCreate() { | ||
| super.onCreate() | ||
| container = AppContainer() | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
container 프로퍼티의 가시성과 불변성을 개선하세요.
container가 public lateinit var로 선언되어 외부에서 변경 가능합니다. 이는 캡슐화를 깨뜨리고 의도하지 않은 수정을 허용할 수 있습니다.
다음 diff를 적용하여 private으로 변경하거나 읽기 전용으로 노출하세요:
class App: Application() {
- lateinit var container: AppContainer
+ private lateinit var container: AppContainer
override fun onCreate() {
super.onCreate()
container = AppContainer()
}
}또는 읽기 전용 접근이 필요한 경우:
class App: Application() {
- lateinit var container: AppContainer
+ private lateinit var _container: AppContainer
+ val container: AppContainer
+ get() = _container
override fun onCreate() {
super.onCreate()
- container = AppContainer()
+ _container = AppContainer()
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| class App: Application() { | |
| lateinit var container: AppContainer | |
| override fun onCreate() { | |
| super.onCreate() | |
| container = AppContainer() | |
| } | |
| class App: Application() { | |
| private lateinit var container: AppContainer | |
| override fun onCreate() { | |
| super.onCreate() | |
| container = AppContainer() | |
| } | |
| } |
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/App.kt around lines 6 to 12,
the container property is declared as a public lateinit var which allows
external mutation; change it to a private mutable backing property and expose a
public read-only accessor (or make it a private lateinit var and add a public
val getter) so callers can only read the container but not replace it;
alternatively initialize it with a private val via lazy in onCreate and expose a
public val for safe, immutable access.
|
|
||
| private val retrofit: Retrofit = Retrofit.Builder() | ||
| .baseUrl(BuildConfig.BASE_URL) | ||
| .baseUrl("http://3.34.136.227:8080") |
There was a problem hiding this comment.
Base URL을 하드코딩하지 말고 BuildConfig 사용 권장
Base URL을 하드코딩하면 다음과 같은 문제가 발생합니다:
- 개발/스테이징/프로덕션 환경 전환이 어려움
- HTTP 사용으로 인한 보안 취약점 (HTTPS 사용 권장)
- 빌드 설정의 유연성 상실
BuildConfig.BASE_URL로 되돌리고, build.gradle에서 환경별로 다른 URL을 설정하는 것이 좋습니다.
- .baseUrl("http://3.34.136.227:8080")
+ .baseUrl(BuildConfig.BASE_URL)Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt
around line 23, the baseUrl is hardcoded to "http://3.34.136.227:8080"; change
it to use BuildConfig.BASE_URL instead and ensure the URL in BuildConfig is
HTTPS, then add per-build-type/baseFlavor BASE_URL entries in module-level
build.gradle (or build.gradle.kts) so dev/staging/prod can supply different
endpoints; update Retrofit setup to reference BuildConfig.BASE_URL and remove
the hardcoded literal.
| override suspend fun getPostDetail(postId: Long): Result<PostResponse> { | ||
| return runCatching { | ||
| val response = apiService.getPostsDetail(postId) | ||
|
|
||
| if (response.success && response.data != null){ | ||
| response.data | ||
| } else{ | ||
| throw Exception(response.message ?: "삭제 실패") | ||
| } | ||
| }.onFailure { error -> | ||
| Log.e("PostRepository", error.message.toString()) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
에러 메시지가 실제 수행하는 작업과 일치하지 않습니다.
getPostDetail에서는 상세 조회 실패 시 "삭제 실패"를, updatePost·deletePost에서는 모두 "생성 실패"를 사용하고 있어 유지보수 시 혼동을 줄 수 있습니다. 각 메서드의 역할에 맞게 메시지를 구분해 주는 편이 좋겠습니다.
- } else{
- throw Exception(response.message ?: "삭제 실패")
+ } else{
+ throw Exception(response.message ?: "게시글 불러오기 실패")
}
...
- }else{
- throw Exception(response.message ?: "생성 실패")
+ }else{
+ throw Exception(response.message ?: "수정 실패")
}
...
- }else{
- throw Exception(response.message ?: "생성 실패")
+ }else{
+ throw Exception(response.message ?: "삭제 실패")
}Also applies to: 56-83
🏁 Script executed:
#!/bin/bash
# Find and examine PostRepositoryImpl.kt file
find . -name "PostRepositoryImpl.kt" -type f 2>/dev/null | head -5Length of output: 87
🏁 Script executed:
#!/bin/bash
# Display the PostRepositoryImpl.kt file content to verify error messages
find . -name "PostRepositoryImpl.kt" -type f -exec cat {} \; 2>/dev/nullLength of output: 3259
에러 메시지를 각 메서드의 실제 작업과 일치시켜 주세요.
getPostDetail, updatePost, deletePost 메서드의 에러 메시지가 실제 수행하는 작업과 맞지 않습니다.
override suspend fun getPostDetail(postId: Long): Result<PostResponse> {
return runCatching {
val response = apiService.getPostsDetail(postId)
if (response.success && response.data != null){
response.data
} else{
- throw Exception(response.message ?: "삭제 실패")
+ throw Exception(response.message ?: "게시글 불러오기 실패")
}
}.onFailure { error ->
Log.e("PostRepository", error.message.toString())
}
}
override suspend fun updatePost(
postId: Long,
request: PostCreateRequest
): Result<PostResponse> {
return runCatching {
val response = apiService.updatePost(postId, request)
if (response.success && response.data != null){
response.data
}else{
- throw Exception(response.message ?: "생성 실패")
+ throw Exception(response.message ?: "수정 실패")
}
}.onFailure { error ->
Log.e("PostRepository", error.message.toString())
}
}
override suspend fun deletePost(postId: Long): Result<PostResponse> {
return runCatching {
val response = apiService.deletePost(postId)
if (response.success && response.data != null){
response.data
}else{
- throw Exception(response.message ?: "생성 실패")
+ throw Exception(response.message ?: "삭제 실패")
}
}.onFailure { error ->
Log.e("PostRepository", error.message.toString())
}
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt
around lines 26 to 38, the thrown/logged error messages are generic or
mismatched for the operation: update the exception message thrown and the Log.e
message in getPostDetail to reflect fetching post details (e.g., "게시물 조회 실패" or
"getPostDetail failed: ...") and similarly audit and correct the messages in
updatePost and deletePost to describe their actual actions (e.g., "게시물 수정
실패"/"updatePost failed: ..." and "게시물 삭제 실패"/"deletePost failed: ..."); ensure
you include response.message if available and pass the error details into the
logger.
| navController.previousBackStackEntry | ||
| ?.savedStateHandle | ||
| ?.set("refreshNeeded", true) |
There was a problem hiding this comment.
🧩 Analysis chain
refreshNeeded 플래그 소비 로직을 확인하세요.
savedStateHandle에 refreshNeeded 플래그를 설정하고 있지만, 제공된 PostListScreen.kt 코드에서 이 플래그를 관찰하고 새로고침을 트리거하는 로직이 보이지 않습니다. PR 설명에서는 "refresh function is executed from the main screen as a side effect"라고 명시되어 있습니다.
다음 스크립트를 실행하여 refreshNeeded 플래그 소비 로직을 확인하세요:
Also applies to: 87-89
🏁 Script executed:
#!/bin/bash
# PostListScreen에서 savedStateHandle 사용 여부 확인
rg -n -A 10 -B 3 'savedStateHandle|refreshNeeded' --type=ktLength of output: 26
🏁 Script executed:
#!/bin/bash
# Search for saveStateHandle and refreshNeeded in Kotlin files using glob pattern
rg -n -A 5 -B 2 'refreshNeeded|savedStateHandle.*get' -g '*.kt'Length of output: 1986
🏁 Script executed:
#!/bin/bash
# Also search for PostListScreen file to understand its structure
fd -e kt -type f -name '*PostListScreen*' | head -20Length of output: 170
🏁 Script executed:
#!/bin/bash
# Search for LaunchedEffect in PostListScreen files
fd -e kt -type f -name '*PostListScreen*' -exec cat {} \;Length of output: 170
🏁 Script executed:
#!/bin/bash
# Find PostListScreen file with corrected fd syntax
fd -e kt -name '*PostListScreen*'Length of output: 230
🏁 Script executed:
#!/bin/bash
# Search more broadly for savedStateHandle reads and refreshNeeded references
rg -n 'savedStateHandle.get|\.get\("refreshNeeded"' -g '*.kt'Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search for refresh function definitions and calls
rg -n -B 2 -A 5 'fun.*refresh|LaunchedEffect' -g '*.kt' | head -100Length of output: 7556
🏁 Script executed:
#!/bin/bash
# Search for all occurrences of refreshNeeded (any read/write operation)
rg -n 'refreshNeeded' -g '*.kt'Length of output: 259
🏁 Script executed:
#!/bin/bash
# Find all screen files in the post package
find . -name '*Screen.kt' -path '*/ui/post/screen/*' 2>/dev/nullLength of output: 335
🏁 Script executed:
#!/bin/bash
# Search for screen composable functions in post package
rg -n '@Composable' -A 2 -g '*.kt' app/src/main/java/com/example/kuit6_android_api/ui/post/screen/ | head -50Length of output: 1261
🏁 Script executed:
#!/bin/bash
# Check if PostListScreen exists anywhere
rg -n 'fun PostListScreen|@Composable.*PostList' -g '*.kt'Length of output: 103
🏁 Script executed:
#!/bin/bash
# Search for where refresh is called as a side effect (LaunchedEffect calling refresh)
rg -n -B 3 -A 3 'viewModel.refresh|\.refresh\(\)' -g '*.kt'Length of output: 0
🏁 Script executed:
#!/bin/bash
# Get the full content of PostListScreen.kt to verify consumption logic
cat ./app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.ktLength of output: 2856
savedStateHandle에서 refreshNeeded 플래그를 읽고 새로고침을 트리거하는 로직이 완전히 누락되었습니다.
PostListScreen.kt:
LaunchedEffect가 없고savedStateHandle을 읽지 않습니다.viewModel.refresh()호출이 없습니다.
PostListViewModel.kt:
refresh()함수가 존재하지만 코드베이스 어디에서도 호출되지 않습니다.
NavGraph.kt (67, 89줄):
refreshNeeded플래그를 설정하기만 하고 읽는 곳이 없습니다.
PostListScreen에서 다음을 구현해야 합니다:
LaunchedEffect로savedStateHandle.getStateFlow("refreshNeeded", false)관찰- 플래그가 true가 되면
viewModel.refresh()호출
또한: 87-89줄에도 동일하게 적용됩니다.
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt
around lines 65-67 and 87-89, the NavGraph currently sets savedStateHandle
"refreshNeeded" but nothing reads it; update PostListScreen to observe that flag
and call the existing PostListViewModel.refresh(): inside PostListScreen add a
LaunchedEffect that collects savedStateHandle.getStateFlow("refreshNeeded",
false) (or use get<Boolean>) and when it becomes true call viewModel.refresh(),
then reset the flag to false via savedStateHandle.set("refreshNeeded", false)
(or setValue) to avoid repeated triggers; ensure this logic is implemented for
both navigation entry points referenced in NavGraph.
| var uploadedImageUrl by remember { mutableStateOf<String?>(null) } | ||
|
|
||
| val imagePickerLauncher = rememberLauncherForActivityResult( | ||
| contract = ActivityResultContracts.GetContent() | ||
| ) { uri: Uri? -> | ||
| selectedImageUri = uri | ||
| uri?.let { | ||
| selectedImageUri = it | ||
|
|
||
| val file = UriUtil.uriToFile(context, it) | ||
| if (file != null) { | ||
| val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) | ||
| val body = MultipartBody.Part.createFormData("file", file.name, requestFile) | ||
| // file을 인자로해서 viewModel의 uploadImage 호출 | ||
| viewModel.uploadImage(body) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
이미지 제거/업로드 실패 후에도 이전 imageUrl 이 요청에 포함될 수 있습니다
uploadedImageUrl는 업로드 성공 시에만 갱신되고, 업로드 실패(ImageUiState.Error, Line [101][109])나 X 버튼으로 이미지를 제거할 때(Line [263][277])는 초기화되지 않습니다. 특히 한 번 업로드에 성공한 뒤 새 이미지를 선택했다가 업로드가 실패하면, 프리뷰는 새 selectedImageUri를 보여주지만(Line [248][261]) [292])이 포함될 수 있어 실제로 저장되는 이미지가 사용자 기대와 다르게 됩니다.PostCreateRequest에는 여전히 이전 uploadedImageUrl(Line [288]
새 이미지를 선택하거나 업로드가 실패/취소되는 경우에는 항상 uploadedImageUrl를 null로 초기화해서 로컬 프리뷰 상태와 서버로 전송되는 값이 일치하도록 맞춰 주는 편이 안전합니다.
@@
- var uploadedImageUrl by remember { mutableStateOf<String?>(null) }
+ var uploadedImageUrl by remember { mutableStateOf<String?>(null) }
@@
- ) { uri: Uri? ->
- uri?.let {
- selectedImageUri = it
-
- val file = UriUtil.uriToFile(context, it)
+ ) { uri: Uri? ->
+ uri?.let {
+ selectedImageUri = it
+ // 새 이미지를 선택하면 이전 업로드 URL을 초기화합니다.
+ uploadedImageUrl = null
+
+ val file = UriUtil.uriToFile(context, it)
@@
- LaunchedEffect(imgUiState) {
- when (imgUiState) {
+ LaunchedEffect(imgUiState) {
+ when (imgUiState) {
is ImageUiState.Success -> {
uploadedImageUrl = (imgUiState as ImageUiState.Success).imgUrl["imageUrl"]
}
is ImageUiState.Error -> {
- Toast
+ // 업로드 실패 시에도 이전 URL이 남지 않도록 초기화합니다.
+ uploadedImageUrl = null
+ Toast
.makeText(
context,
(imgUiState as ImageUiState.Error).message,
Toast.LENGTH_SHORT
)
.show()
@@
- IconButton(
- onClick = {
- selectedImageUri = null
- viewModel.clearUploadedImageUrl()
- },
+ IconButton(
+ onClick = {
+ selectedImageUri = null
+ // 사용자가 이미지를 제거하면 로컬/뷰모델 상태를 모두 비웁니다.
+ uploadedImageUrl = null
+ viewModel.clearUploadedImageUrl()
+ },이렇게 하면 업로드 성공, 실패, 이미지 제거 상황 모두에서 프리뷰와 서버 요청 간의 상태가 일관되게 유지됩니다.
Also applies to: 96-112, 248-279, 285-295
| var selectedImageUri by remember { mutableStateOf<Uri?>(null)} | ||
| var uploadedImageUrl by remember { mutableStateOf<String?>(null) } | ||
| var isLoaded by remember { mutableStateOf(false) } | ||
| val scope = rememberCoroutineScope() | ||
|
|
||
| val imagePickerLauncher = rememberLauncherForActivityResult( | ||
| contract = ActivityResultContracts.GetContent() | ||
| ) { uri: Uri? -> | ||
| selectedImageUri = uri | ||
| uri?.let { | ||
| selectedImageUri = it | ||
| val file = UriUtil.uriToFile(context, it) | ||
| if (file != null) { | ||
| val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull()) | ||
| val body = MultipartBody.Part.createFormData("file", file.name, requestFile) | ||
|
|
||
| viewModel.uploadImage(body) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
이미지 변경 후 업로드 실패 시 이전 imageUrl 이 계속 전송될 수 있습니다
PostEditScreen에서도 uploadedImageUrl는 업로드 성공 시에만 갱신되고(Line [100][104]), 새 이미지를 선택할 때나 업로드 실패 시에는 초기화되지 않습니다(Line [84][93], [105][111]). 이미 한 번 업로드에 성공해 [207]) 실제 수정 요청에서는 여전히 이전 uploadedImageUrl가 설정된 상태에서 새 이미지를 선택했다가 업로드가 실패하면, 프리뷰는 새 selectedImageUri를 보여주지만(Line [194]uploadedImageUrl가 사용되어(Line [245]~[251]) 저장 결과가 사용자 기대와 달라질 수 있습니다.
새 이미지를 선택하거나 업로드가 실패하는 경우에는 항상 uploadedImageUrl를 null로 초기화해 주는 것이 안전합니다.
@@
- ) { uri: Uri? ->
- uri?.let {
- selectedImageUri = it
- val file = UriUtil.uriToFile(context, it)
+ ) { uri: Uri? ->
+ uri?.let {
+ selectedImageUri = it
+ // 새 이미지를 선택하면 이전 업로드 URL을 초기화합니다.
+ uploadedImageUrl = null
+
+ val file = UriUtil.uriToFile(context, it)
@@
- LaunchedEffect(imgUiState) {
- when (imgUiState) {
+ LaunchedEffect(imgUiState) {
+ when (imgUiState) {
is ImageUiState.Success -> {
uploadedImageUrl = (imgUiState as ImageUiState.Success).imgUrl["imageUrl"]
}
is ImageUiState.Error -> {
- Toast.makeText(
+ // 업로드 실패 시에도 이전 URL이 남지 않도록 초기화합니다.
+ uploadedImageUrl = null
+ Toast.makeText(
context,
(imgUiState as ImageUiState.Error).message,
Toast.LENGTH_SHORT
).show()이렇게 하면 "이미지 A로 저장 후 → 이미지 B 선택 → 업로드 실패 → 수정하기" 시나리오에서도 더 이상 이미지 A URL이 다시 전송되지 않습니다.
Also applies to: 100-113, 194-228, 242-252
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt
around lines 77-94 (and related 100-113, 194-228, 242-252): currently
uploadedImageUrl is only set on successful upload, so when the user selects a
new image (or the upload fails) the old uploadedImageUrl may remain and be sent
with the save request; fix by resetting uploadedImageUrl = null whenever a new
image is selected (inside the imagePickerLauncher callback before starting
upload) and also ensure the viewModel upload failure path clears
uploadedImageUrl (or expose an upload state callback to set it null on error) so
the preview can show selectedImageUri while the persisted image URL is cleared
and the save request will not send the stale URL.
| sealed class PostUIState { | ||
| data object Loading: PostUIState() | ||
|
|
||
| data class Success( | ||
| val post: PostResponse | ||
| ): PostUIState() | ||
|
|
||
| data class Error( | ||
| val message: String | ||
| ): PostUIState() | ||
| } No newline at end of file |
There was a problem hiding this comment.
🧩 Analysis chain
네이밍 일관성 문제를 수정해주세요.
이 클래스는 PostUIState로 명명되었지만, PR 요약에 언급된 다른 UI 상태 클래스들(PostListUiState, PostDetailUiState, PostCreateUiState 등)은 UiState (소문자 'i')를 사용합니다. 코드베이스 전체에서 일관된 네이밍 컨벤션을 유지하는 것이 중요합니다.
다음 diff를 적용하여 네이밍을 일관되게 수정하세요:
-sealed class PostUIState {
- data object Loading: PostUIState()
+sealed class PostUiState {
+ data object Loading: PostUiState()
data class Success(
val post: PostResponse
- ): PostUIState()
+ ): PostUiState()
data class Error(
val message: String
- ): PostUIState()
+ ): PostUiState()
}또한, 이 PostUIState 클래스가 PostDetailUiState와 중복되거나 겹치는 용도로 사용되지 않는지 확인이 필요합니다. 다음 스크립트를 실행하여 두 상태 클래스의 사용처를 확인해주세요:
🏁 Script executed:
#!/bin/bash
# Description: PostUIState와 PostDetailUiState의 사용처를 확인하여 중복 여부를 검증합니다.
# PostUIState 사용처 검색
echo "=== PostUIState 사용처 ==="
rg -n "PostUIState" --type kotlin -C 3
echo -e "\n=== PostDetailUiState 사용처 ==="
rg -n "PostDetailUiState" --type kotlin -C 3Length of output: 16197
PostUIState는 사용되지 않는 중복 코드입니다. 삭제하거나 용도를 명확히 해주세요.
검색 결과에 따르면 PostUIState는 자신의 파일에서만 정의되고 코드베이스 전체에서 사용되지 않습니다. 반면 PostDetailUiState는 PostDetailViewModel과 PostDetailScreen에서 활발하게 사용되고 있으며, 두 클래스의 구조가 정확히 동일합니다(Loading, Success, Error).
선택지:
- Dead code 제거: 불필요한 중복 코드이므로
PostUIState.kt파일 자체를 삭제하세요. - 명확한 용도 구분: 만약
PostUIState가 다른 목적(예: 게시글 목록 화면)으로 사용될 예정이라면, 그 용도를 명시하고 관련 ViewModel/Screen에서 사용되도록 수정하세요.
만약 파일을 유지한다면, 최소한 다음 네이밍 일관성 수정은 필수입니다:
PostUIState→PostUiState(다른 UI 상태 클래스들과 일관성 유지)
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostUIState.kt
around lines 5-15, PostUIState is duplicate and unused; either delete the file
to remove dead code, or if it’s intended for a different screen (e.g., post
list) explicitly wire it into the corresponding ViewModel/Screen and update its
purpose in comments; if you keep it, rename to PostUiState to match naming
conventions, update any references, and remove unused imports so the class is
actually used and consistent with PostDetailUiState.
| val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] | ||
| as App | ||
|
|
||
| val postRepository = application.container.postRepository | ||
| create(postRepository) |
There was a problem hiding this comment.
Application 타입 캐스팅에 안전성 검사를 추가하세요.
as App 형변환이 실패하면 ClassCastException이 발생합니다. Application이 App 타입이 아닌 경우를 대비한 안전장치가 필요합니다.
다음 diff를 적용하여 안전한 캐스팅을 사용하세요:
initializer {
val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]
- as App
+ as? App
+ ?: throw IllegalStateException("Application must be an instance of App")
val postRepository = application.container.postRepository
create(postRepository)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] | |
| as App | |
| val postRepository = application.container.postRepository | |
| create(postRepository) | |
| val application = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] | |
| as? App | |
| ?: throw IllegalStateException("Application must be an instance of App") | |
| val postRepository = application.container.postRepository | |
| create(postRepository) |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt
around lines 14 to 18, the code performs an unsafe cast "as App" on the
Application which can throw ClassCastException; replace it with a safe cast (as?
App) and handle the null result (e.g., throw an IllegalStateException or return
a sensible fallback) before accessing application.container.postRepository so
the factory fails with a clear message instead of a ClassCastException.
📝 미션
미션: PostViewModel 각 screen에 맞는 ViewModel로 분리
🙋 구현에 대한 설명
수동 주입(App Container) 구현
Repository 패턴 사용
ViewModel 분리
PostEditViewModel
PostDetailViewModel (삭제까지 같이 하시면 됩니다)
PostCreateViewModel
UiState 구현
PostViewModel 삭제
PostListViewModel의 refresh 함수를 메인화면에서 sideEffect로 항상 실행시키기
Summary by CodeRabbit
릴리즈 노트
새로운 기능
리팩토링