Skip to content

Conversation

@1hyok
Copy link

@1hyok 1hyok commented Nov 13, 2025

Summary by CodeRabbit

릴리스 노트

  • 개선사항
    • 게시물 목록 새로고침이 화면 라이프사이클에 연동되어 더욱 효율적으로 작동합니다.
    • 애플리케이션 상태 관리 구조를 개선하여 게시물 조회, 작성, 수정, 삭제 기능의 안정성이 강화되었습니다.
    • 의존성 관리 체계를 개선하여 전반적인 앱 성능이 최적화되었습니다.

1hyok added 17 commits November 12, 2025 17:39
기존 ViewModel에서 직접 API 통신을 하던 구조를 Repository 패턴을 적용하여 개선했습니다. 의존성 주입(DI)을 위해 Application 클래스와 AppContainer를 도입하고, ViewModelProvider.Factory를 사용하여 ViewModel에 Repository를 주입하도록 변경했습니다.

*   **App, di, repository 패키지 추가**:
    *   `App.kt`: Application 클래스를 생성하여 DI 컨테이너(`AppContainer`)를 초기화합니다.
    *   `AppContainer.kt`: `PostRepository`의 인스턴스를 생성하고 관리합니다.
    *   `PostRepository.kt`, `PostRepositoryImpl.kt`: API 호출 로직을 ViewModel에서 분리하여 Repository 계층으로 이동했습니다.

*   **ViewModel 리팩토링**:
    *   `PostListViewModel.kt`: 게시글 목록 조회를 담당하는 ViewModel을 새로 생성하고, `StateFlow`를 사용하여 UI 상태(`PostListUiState`)를 관리합니다.
    *   `PostViewModel.kt`: 게시글 목록 조회(`getPosts`) 관련 로직을 `PostListViewModel`으로 이전했습니다.
    *   `postViewModelFactory.kt`: `PostRepository`를 ViewModel에 주입하기 위한 `ViewModelProvider.Factory`를 구현했습니다.

*   **UI 및 화면 로직 수정**:
    *   `PostListScreen.kt`: `PostListViewModel`과 `PostListUiState`를 사용하도록 변경하여, 로딩, 성공, 에러 상태에 따라 다른 UI를 표시하도록 구현했습니다.
    *   `NavGraph.kt`: `PostListScreen`에 `PostListViewModel`을 `viewModelFactory`를 통해 주입하도록 수정했습니다.

*   **기타 변경 사항**:
    *   `AndroidManifest.xml`: `Application` 클래스를 `App`으로 지정했습니다.
    *   `UriUtils.kt`: `ui.post.viewmodel` 패키지에서 `util` 패키지로 파일을 이동했습니다.
기존의 단일 `PostViewModel`을 `PostCreateViewModel`, `PostDetailViewModel`, `PostEditViewModel`로 분리하여 각 화면의 역할을 명확히 했습니다. 또한, `PostRepository`를 도입하여 데이터 소스를 추상화하고 ViewModel과 데이터 계층 간의 의존성을 분리했습니다.

*   **Repository 추가**:
    *   `PostRepository`: `ApiService`를 사용하여 게시글 CRUD 및 이미지 업로드 API를 호출하는 로직을 캡슐화했습니다.

*   **ViewModel 분리**:
    *   `PostCreateViewModel`: 게시글 생성 및 이미지 업로드 기능을 담당합니다.
    *   `PostDetailViewModel`: 게시글 상세 조회 및 삭제 기능을 담당합니다.
    *   `PostEditViewModel`: 게시글 수정, 상세 조회 및 이미지 업로드 기능을 담당합니다.

*   **의존성 주입(DI)**:
    *   `AppContainer`: `PostRepository`의 인스턴스를 생성하고 관리하여 ViewModel에 주입하는 역할을 합니다.
    *   각 Screen(`PostCreateScreen`, `PostDetailScreen`, `PostEditScreen`)에서는 `AppContainer`를 통해 해당 화면에 맞는 ViewModel을 주입받도록 수정했습니다.
기존의 단일 `PostViewModel`을 `PostListViewModel`으로 이름을 변경하고 목록 조회 기능만 남도록 역할을 축소했습니다. 또한, 각 ViewModel에서 개별적으로 관리하던 상태 변수들을 `PostListUiState`, `PostCreateUiState`, `PostDetailUiState`, `PostEditUiState`와 같은 데이터 클래스로 통합하여 상태 관리를 개선했습니다.

*   **ViewModel 분리 및 리팩토링**:
    *   `PostViewModel.kt`: 기존의 다기능 ViewModel을 삭제했습니다.
    *   `PostListViewModel.kt`: 게시글 목록 조회를 담당하는 ViewModel을 새로 생성하고, `PostListUiState`를 사용하여 UI 상태를 관리합니다. `getPosts()` 메서드는 `refresh()`로 이름을 변경했습니다.
    *   `PostCreateViewModel`, `PostDetailViewModel`, `PostEditViewModel`: 각 ViewModel의 상태 변수들(`uploadedImageUrl`, `isUploading`, `postDetail` 등)을 `UiState` 데이터 클래스로 캡슐화하여 관리하도록 수정했습니다.

*   **UI 및 화면 로직 수정**:
    *   `PostListScreen.kt`: `PostListViewModel`과 `PostListUiState`를 사용하도록 변경했습니다.
    *   `PostCreateScreen.kt`, `PostDetailScreen.kt`, `PostEditScreen.kt`: 각 화면에서 ViewModel의 `UiState`를 참조하여 UI를 렌더링하도록 수정했습니다.
ViewModel을 주입하는 방식을 익명 객체 `ViewModelProvider.Factory`를 사용하도록 변경하여 코드를 간소화했습니다. 또한, `PostListScreen`에 `DisposableEffect`와 `LifecycleEventObserver`를 사용하여 화면이 다시 활성화될 때마다 게시글 목록을 자동으로 새로고침하는 기능을 추가했습니다.

*   **ViewModel 팩토리 리팩토링**:
    *   `PostCreateScreen`, `PostDetailScreen`, `PostEditScreen`, `PostListScreen`: `viewModel()` 함수에 익명 클래스로 `ViewModelProvider.Factory`를 구현하여 ViewModel 인스턴스를 생성하도록 수정했습니다.

*   **게시글 목록 자동 새로고침**:
    *   `PostListScreen.kt`: 화면의 생명주기(Lifecycle)가 `ON_RESUME` 상태일 때 `viewModel.refresh()`를 호출하도록 `DisposableEffect`를 추가했습니다. 이를 통해 다른 화면에서 게시글을 생성, 수정, 삭제하고 목록 화면으로 돌아왔을 때 변경 사항이 즉시 반영됩니다.

*   **ViewModel 예외 처리 개선**:
    *   `PostCreateViewModel.kt`, `PostEditViewModel.kt`: 이미지 업로드 시 파일 변환에 실패하는 경우, `runCatching` 대신 `if (file == null)` 체크를 통해 명시적으로 예외를 처리하도록 수정했습니다.
`PostCreateScreen`에서 ViewModel을 생성하는 방식을 수정했습니다.

*   **PostCreateScreen.kt**: `ViewModelProvider.Factory`를 사용하여 `PostCreateViewModel`을 생성하도록 변경했습니다. 이를 통해 수동으로 생성한 `AppContainer`에서 Repository를 가져와 ViewModel에 주입할 수 있도록 구조를 개선했습니다.
기존의 단일 PostViewModel을 PostCreateViewModel, PostDetailViewModel, PostEditViewModel로 분리하여 각 화면의 역할을 명확히 했습니다. 또한, PostRepository를 도입하여 데이터 소스를 추상화하고 ViewModel과 데이터 계층 간의 의존성을 분리했습니다.

*   **Repository 추가**:PostRepository: ApiService를 사용하여 게시글 CRUD 및 이미지 업로드 API를 호출하는 로직을 캡슐화했습니다.

*   **ViewModel 분리**:PostCreateViewModel: 게시글 생성 및 이미지 업로드 기능을 담당합니다.PostDetailViewModel: 게시글 상세 조회 및 삭제 기능을 담당합니다.PostEditViewModel: 게시글 수정, 상세 조회 및 이미지 업로드 기능을 담당합니다.

*   **의존성 주입(DI)**:AppContainer: PostRepository의 인스턴스를 생성하고 관리하여 ViewModel에 주입하는 역할을 합니다.각 Screen(PostCreateScreen, PostDetailScreen, PostEditScreen)에서는 AppContainer를 통해 해당 화면에 맞는 ViewModel을 주입받도록 수정했습니다.
기존의 단일 `PostViewModel`을 `PostListViewModel`으로 이름을 변경하고 목록 조회 기능만 남도록 역할을 축소했습니다. 또한, 각 ViewModel에서 개별적으로 관리하던 상태 변수들을 `PostListUiState`, `PostCreateUiState`, `PostDetailUiState`, `PostEditUiState`와 같은 데이터 클래스로 통합하여 상태 관리를 개선했습니다.

*   **ViewModel 분리 및 리팩토링**:
    *   `PostViewModel.kt`: 기존의 다기능 ViewModel을 삭제했습니다.
    *   `PostListViewModel.kt`: 게시글 목록 조회를 담당하는 ViewModel을 새로 생성하고, `PostListUiState`를 사용하여 UI 상태를 관리합니다. `getPosts()` 메서드는 `refresh()`로 이름을 변경했습니다.
    *   `PostCreateViewModel`, `PostDetailViewModel`, `PostEditViewModel`: 각 ViewModel의 상태 변수들(`uploadedImageUrl`, `isUploading`, `postDetail` 등)을 `UiState` 데이터 클래스로 캡슐화하여 관리하도록 수정했습니다.

*   **UI 및 화면 로직 수정**:
    *   `PostListScreen.kt`: `PostListViewModel`과 `PostListUiState`를 사용하도록 변경했습니다.
    *   `PostCreateScreen.kt`, `PostDetailScreen.kt`, `PostEditScreen.kt`: 각 화면에서 ViewModel의 `UiState`를 참조하여 UI를 렌더링하도록 수정했습니다.
ViewModel을 주입하는 방식을 익명 객체 `ViewModelProvider.Factory`를 사용하도록 변경하여 코드를 간소화했습니다. 또한, `PostListScreen`에 `DisposableEffect`와 `LifecycleEventObserver`를 사용하여 화면이 다시 활성화될 때마다 게시글 목록을 자동으로 새로고침하는 기능을 추가했습니다.

*   **ViewModel 팩토리 리팩토링**:
    *   `PostCreateScreen`, `PostDetailScreen`, `PostEditScreen`, `PostListScreen`: `viewModel()` 함수에 익명 클래스로 `ViewModelProvider.Factory`를 구현하여 ViewModel 인스턴스를 생성하도록 수정했습니다.

*   **게시글 목록 자동 새로고침**:
    *   `PostListScreen.kt`: 화면의 생명주기(Lifecycle)가 `ON_RESUME` 상태일 때 `viewModel.refresh()`를 호출하도록 `DisposableEffect`를 추가했습니다. 이를 통해 다른 화면에서 게시글을 생성, 수정, 삭제하고 목록 화면으로 돌아왔을 때 변경 사항이 즉시 반영됩니다.

*   **ViewModel 예외 처리 개선**:
    *   `PostCreateViewModel.kt`, `PostEditViewModel.kt`: 이미지 업로드 시 파일 변환에 실패하는 경우, `runCatching` 대신 `if (file == null)` 체크를 통해 명시적으로 예외를 처리하도록 수정했습니다.
`PostCreateScreen`에서 ViewModel을 생성하는 방식을 수정했습니다.

*   **PostCreateScreen.kt**: `ViewModelProvider.Factory`를 사용하여 `PostCreateViewModel`을 생성하도록 변경했습니다. 이를 통해 수동으로 생성한 `AppContainer`에서 Repository를 가져와 ViewModel에 주입할 수 있도록 구조를 개선했습니다.
Android Gradle Plugin(agp)의 버전을 8.13.0에서 8.13.1로 업데이트했습니다.
각 화면(Screen)에서 `ViewModel`을 주입받는 부분에 `postViewModelFactory`를 통해 `Repository`가 수동으로 주입된다는 내용의 주석을 추가했습니다.

*   `PostCreateScreen.kt`
*   `PostDetailScreen.kt`
*   `PostEditScreen.kt`
*   `PostViewModelFactory.kt`
기존의 단일 `PostViewModel`을 기능별로 분리하여 각 화면의 역할을 명확히 했습니다. 또한, `PostRepository`를 도입하여 데이터 소스를 추상화하고 ViewModel과 데이터 계층 간의 의존성을 분리했습니다.

*   **Repository 추가 및 수정**:
    *   `PostRepository`: 게시글 CRUD(생성, 상세 조회, 수정, 삭제) 및 이미지 업로드 기능을 위한 메서드를 추가했습니다.
    *   `PostRepositoryImpl`: 추가된 인터페이스 메서드들을 구현했습니다.

*   **ViewModel 분리**:
    *   `PostViewModel`을 삭제하고, 기능에 따라 아래의 ViewModel들로 분리했습니다.
    *   `PostCreateViewModel`: 게시글 생성 및 이미지 업로드 기능을 담당합니다.
    *   `PostDetailViewModel`: 게시글 상세 조회 및 삭제 기능을 담당합니다.
    *   `PostEditViewModel`: 게시글 수정, 상세 조회 및 이미지 업로드 기능을 담당합니다.

*   **의존성 주입(DI)**:
    *   `AppContainer`: `PostRepository`의 인스턴스를 생성하고 관리하여 ViewModel에 주입하는 역할을 합니다.
    *   `NavGraph`: 각 화면(`PostCreateScreen`, `PostDetailScreen`, `PostEditScreen`)에서 `AppContainer`를 통해 해당 화면에 맞는 ViewModel을 주입받도록 수정했습니다.
`PostListViewModel`에서 `StateFlow`를 사용하여 UI 상태를 관리하도록 변경하고, `PostListScreen`에서는 로딩, 성공, 에러 상태에 따라 다른 UI를 표시하도록 개선했습니다.

*   **PostListViewModel.kt**:
    *   기존 `mutableStateOf`로 관리하던 `uiState`를 `StateFlow`로 변경했습니다.
    *   게시글 목록 조회 결과에 따라 `Loading`, `Success`, `Error` 상태를 갖는 `PostListUiState`를 `emit`하도록 수정했습니다.
    *   게시글을 불러오는 `loadPosts()` 함수를 추가하고, `refresh()`에서 이를 호출하도록 변경했습니다.

*   **PostListScreen.kt**:
    *   `collectAsState()`를 사용하여 `ViewModel`의 `uiState`를 구독하도록 변경했습니다.
    *   `when` 문을 사용하여 `PostListUiState`의 상태(Loading, Success, Error)에 따라 `CircularProgressIndicator` 또는 `LazyColumn`을 표시하도록 UI 로직을 수정했습니다.
    *   기존 `DisposableEffect`를 사용한 생명주기 기반의 새로고침 로직을 제거하고, `LaunchedEffect`를 사용하여 화면 진입 시 데이터를 불러오도록 단순화했습니다.
DI 컨테이너인 `AppContainer`의 패키지 경로를 `di`에서 `data/di`로 변경하여 데이터 계층의 일부임을 명확히 했습니다. 또한, `lazy` 초기화를 사용하여 `AppContainer` 내에서 `ApiService`와 `PostRepository`의 인스턴스 생성을 최적화했습니다.

*   **`AppContainer` 패키지 이동**:
    *   기존 `di/AppContainer.kt` 파일을 `data/di/AppContainer.kt`로 이동했습니다.
*   **`lazy` 초기화 적용**:
    *   `AppContainer` 내의 `apiService`와 `postRepository` 프로퍼티를 `lazy`를 사용하여 초기화하도록 수정했습니다. 이를 통해 해당 인스턴스들이 실제로 처음 사용될 때 생성되도록 변경했습니다.
*   **`PostRepository` 인터페이스 확장**:
    *   `getPostDetail`, `createPost`, `deletePost`, `editPost` 함수를 추가하여 게시글의 상세 조회 및 CRUD 기능을 위한 메서드를 정의했습니다.
`PostListScreen`의 `LaunchedEffect` 블록에 Side Effect에 대한 설명을 주석으로 추가하여 코드의 가독성을 높였습니다.
@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Expected object, received boolean at "reviews.auto_review"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

저장소 기반 아키텍처를 도입하여 AppContainer를 통해 ApiService와 PostRepository를 중앙화했습니다. PostViewModel을 네 가지 전용 ViewModel(PostListViewModel, PostCreateViewModel, PostDetailViewModel, PostEditViewModel)으로 분리하고, 대응하는 UI 상태 클래스를 추가했습니다. PostRepository 인터페이스와 구현체를 도입했으며, 네비게이션과 화면을 팩토리 기반 의존성 주입으로 업데이트했습니다.

Changes

Cohort / File(s) Change Summary
의존성 주입 컨테이너
app/src/main/java/com/example/kuit6_android_api/App.kt, app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt, app/src/main/AndroidManifest.xml
새로운 Application 클래스 App을 도입하여 AppContainer를 초기화합니다. AppContainer는 ApiService와 PostRepository를 중앙화하여 관리합니다. AndroidManifest.xml에 android:name=".App" 속성을 추가했습니다.
Repository 패턴
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt, app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt
새로운 PostRepository 인터페이스를 정의하고, PostRepositoryImpl에서 CRUD 연산과 이미지 업로드를 구현했습니다. 모든 작업은 Result로 감싸져 있습니다.
Post List ViewModel
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt
새로운 PostListViewModel은 StateFlow<PostListUiState>로 UI 상태를 관리합니다. 세 가지 상태(Loading, Success, Error)를 제공합니다.
Post Create ViewModel
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt
새로운 PostCreateViewModel은 게시글 작성과 이미지 업로드 흐름을 관리합니다. PostCreateUiState로 업로드된 이미지 URL과 업로드 상태를 추적합니다.
Post Detail ViewModel
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt
새로운 PostDetailViewModel은 게시글 상세 조회와 삭제 기능을 관리합니다. PostDetailUiState로 게시글 데이터를 보유합니다.
Post Edit ViewModel
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt
새로운 PostEditViewModel은 게시글 편집과 이미지 업로드를 관리합니다. PostEditUiState로 상세 정보, 업로드된 이미지, 업로드 상태를 추적합니다.
ViewModel 팩토리
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt
새로운 postViewModelFactory 함수는 AppContainer에서 PostRepository를 주입하여 ViewModel을 생성합니다.
화면 업데이트
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt
각 화면을 해당하는 전용 ViewModel으로 업데이트하고, 팩토리 기반 주입을 적용했습니다. uiState를 통해 UI 상태에 접근하도록 변경했습니다.
네비게이션 업데이트
app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt
각 화면에 해당하는 ViewModel 팩토리를 사용하여 의존성 주입을 설정했습니다.
ViewModel 제거
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
모놀리식 PostViewModel을 완전히 제거했습니다.
유틸리티 이동
app/src/main/java/com/example/kuit6_android_api/util/UriUtils.kt
UriUtils를 com.example.kuit6_android_api.ui.post.viewmodel 패키지에서 com.example.kuit6_android_api.util로 이동했습니다.
문서화
PROJECT_CONTEXT.md
프로젝트 컨텍스트, 브랜치 구조, 미션 요구사항 체크리스트, 개발 원칙을 설명하는 새로운 문서를 추가했습니다.
Gradle 버전 업데이트
gradle/libs.versions.toml
AGP 버전을 8.13.0에서 8.13.1로 업데이트했습니다.

Sequence Diagram(s)

sequenceDiagram
    participant Screen as Post Screen
    participant ViewModel as ViewModel<br/>(Create/Detail/Edit/List)
    participant Factory as PostViewModelFactory
    participant AppContainer as AppContainer
    participant Repository as PostRepository<br/>(Impl)
    participant API as ApiService

    Screen->>Factory: viewModel(factory=...)
    Factory->>AppContainer: App.container
    AppContainer-->>Factory: PostRepository
    Factory->>ViewModel: ViewModel(repository)
    ViewModel-->>Screen: Injected ViewModel

    Screen->>ViewModel: 사용자 액션<br/>(조회/생성/수정/삭제)
    ViewModel->>Repository: 저장소 메서드 호출
    Repository->>API: REST 호출
    API-->>Repository: 응답
    Repository-->>ViewModel: Result\<T\>
    ViewModel->>ViewModel: uiState 업데이트
    ViewModel-->>Screen: 상태 변경 감지
    Screen->>Screen: UI 렌더링
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • 특별 주의 사항:
    • PostRepositoryImpl 구현 검토: API 호출 에러 처리 및 Result 래핑 로직이 모든 메서드에서 일관성 있게 적용되었는지 확인 필요
    • AppContainer의 지연 초기화: apiService와 postRepository의 lazy 초기화가 스레드 안전성을 보장하는지 검증 필요
    • ViewModel 팩토리 패턴: postViewModelFactory가 올바르게 reified 타입 파라미터를 사용하여 타입 안전성을 제공하는지 확인
    • UI 상태 관리 일관성: PostListViewModel (StateFlow)과 PostCreateViewModel/PostDetailViewModel/PostEditViewModel (mutableStateOf)의 상태 관리 방식 차이가 의도적인지 검증
    • PostViewModel 제거의 영향범위: 모든 화면과 네비게이션에서 PostViewModel 참조가 완전히 제거되었는지 확인

Possibly related PRs

  • 6주차 미션 #16: ApiService, RetrofitClient, PostResponse, BaseResponse 등 네트워크 모델을 도입하여, 본 PR의 PostRepositoryImpl, AppContainer, ViewModel이 직접 의존하는 기반을 제공합니다.
  • 6주차 미션 / 안드로이드 2조 권태우 #18: 저장소/ViewModel 계층을 구축하고 Post 관련 기능을 분리하는 본 PR에 직접적으로 의존합니다.
  • 6주차 미션 / 안드로이드 2조 서아영 #19: ApiService/RetrofitClient와 PostViewModel을 도입하는 PR로, 본 PR에서 PostViewModel을 제거하고 PostRepository/PostRepositoryImpl로 대체하는 리팩토링의 기반이 됩니다.

Poem

🐰 저장소의 정원을 가꾸며,
모놀리식 나무를 네 가닥으로 쪼개고,
AppContainer에 씨앗을 심어,
의존성의 뿌리 깊이 자라나네.
상태의 꽃이 피어나니,
아름다운 구조의 계절 도래! 🌱✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning PR 제목이 명확한 변경 사항을 설명하지 못하고 있으며, 저자명과 주차 번호만 포함하고 있습니다. PR 제목을 구체적인 변경 내용을 반영하도록 수정해주세요. 예: '저장소 패턴 및 ViewModel 분리 구현 (AppContainer, 수정된 PostViewModel)' 또는 '의존성 주입 및 기능별 ViewModel 아키텍처 리팩토링'
Docstring Coverage ⚠️ Warning Docstring coverage is 3.23% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@alswlekk
Copy link
Contributor

_ No description provided. _

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)

  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 14, 2025

@alswlekk: I'll review the changes in this PR right away! 🐰

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (4)
app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt (1)

5-15: 코드 스타일 일관성을 개선하세요.

sealed class의 서브타입 선언에서 콜론 앞 공백이 일관되지 않습니다. Kotlin 코딩 컨벤션에 따라 콜론 앞에 공백을 추가하는 것을 권장합니다.

다음 diff를 적용하여 스타일 일관성을 개선하세요:

 sealed class PostListUiState {
-    data object Loading: PostListUiState()
+    data object Loading : PostListUiState()
 
     data class Success(
         val posts: List<PostResponse>
-    ):PostListUiState()
+    ) : PostListUiState()
 
     data class Error(
         val message: String
     ): PostListUiState()
+    ) : PostListUiState()
 }
BRANCH_DIFFERENCES.md (1)

23-25: 마크다운 리스트 들여쓰기를 수정하세요.

중첩 리스트 항목의 들여쓰기가 마크다운 컨벤션(2칸)을 따르지 않습니다.

다음 diff를 적용하여 들여쓰기를 수정하세요:

 **주요 내용:**
 ```kotlin
 class AppContainer {
-    val apiService: ApiService = RetrofitClient.apiService
-    val postRepository: PostRepository = PostRepositoryImpl(apiService)
+  val apiService: ApiService = RetrofitClient.apiService
+  val postRepository: PostRepository = PostRepositoryImpl(apiService)
 }
app/src/main/java/com/example/kuit6_android_api/App.kt (1)

6-6: 코틀린 코딩 컨벤션을 따라 공백을 추가하세요.

클래스 선언에서 콜론 앞에 공백이 없습니다. Kotlin 코딩 컨벤션에 따라 공백을 추가하는 것을 권장합니다.

다음 diff를 적용하세요:

-class App: Application() {
+class App : Application() {
PROJECT_CONTEXT.md (1)

23-25: 마크다운 리스트 들여쓰기를 표준에 맞춰 정리하는 것을 권장합니다.

중첩된 리스트 항목의 들여쓰기가 4칸으로 되어 있는데, 마크다운 표준은 2칸 들여쓰기를 권장합니다.

다음 diff를 적용하여 들여쓰기를 수정할 수 있습니다:

 - [x] ViewModel 분리
-    - PostEditViewModel
-    - PostDetailViewModel (삭제까지 같이 하시면 됩니다)
-    - PostCreateViewModel
+  - PostEditViewModel
+  - PostDetailViewModel (삭제까지 같이 하시면 됩니다)
+  - PostCreateViewModel
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 045ed9c and 874812c.

📒 Files selected for processing (21)
  • BRANCH_DIFFERENCES.md (1 hunks)
  • PROJECT_CONTEXT.md (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/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 (5 hunks)
  • app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt (2 hunks)
  • app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt (4 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/PostListUiState.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)
  • app/src/main/java/com/example/kuit6_android_api/util/UriUtils.kt (2 hunks)
  • gradle/libs.versions.toml (1 hunks)
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
PROJECT_CONTEXT.md

23-23: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


25-25: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

BRANCH_DIFFERENCES.md

23-23: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


25-25: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🔇 Additional comments (13)
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt (1)

7-25: 잘 설계된 Repository 인터페이스입니다.

Result 타입을 활용한 에러 핸들링과 명확한 메서드 시그니처로 깔끔한 Repository 패턴 구현입니다. suspend 함수로 선언하여 코루틴 사용을 고려한 점도 좋습니다.

app/src/main/AndroidManifest.xml (1)

17-17: Application 클래스 등록이 올바르게 되었습니다.

DI 컨테이너 초기화를 위한 커스텀 Application 클래스 등록이 정확합니다. App 클래스가 onCreate에서 AppContainer를 초기화하여 앱 전역에서 Repository 접근이 가능하도록 합니다.

app/src/main/java/com/example/kuit6_android_api/util/UriUtils.kt (1)

1-1: 적절한 패키지 재배치입니다.

UriUtils를 viewmodel 패키지에서 util 패키지로 이동한 것은 좋은 리팩토링입니다. 유틸리티 클래스가 올바른 위치에 배치되어 코드 구조가 개선되었습니다.

app/src/main/java/com/example/kuit6_android_api/App.kt (1)

7-12: DI 컨테이너 초기화가 잘 구현되었습니다.

Application 클래스에서 AppContainer를 초기화하여 앱 전역에서 의존성 주입을 사용할 수 있도록 한 구조가 적절합니다. lateinit var 사용도 Application 라이프사이클 상 onCreate에서 반드시 초기화되므로 안전합니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt (1)

10-26: 우수한 Factory 패턴 구현입니다.

Repository를 ViewModel에 주입하기 위한 Factory 함수가 잘 구현되었습니다. reified 타입 파라미터를 활용한 제네릭 구현과 ViewModelProvider의 APPLICATION_KEY를 통한 안전한 App 접근이 인상적입니다. 한국어 주석도 명확하여 코드 의도를 잘 전달하고 있습니다.

gradle/libs.versions.toml (1)

2-2: Let me verify AGP 8.13.1 using web search to check if this version exists and whether there are any known security issues:

Let me search more specifically for AGP 8.13.1 patch version:

Let me perform one final targeted search to definitively confirm AGP 8.13.1 availability:


AGP 8.13.1 버전은 공식 릴리스로 존재하지 않는 것으로 보입니다.

공식 Android Developers 문서는 8.13.0 버전을 참조합니다. 문서 최종 업데이트: 2025-11-06. 웹 검색 결과, 8.13.1 패치 버전에 대한 공식 기록이나 릴리스 노트를 찾을 수 없습니다. AGP 8.13은 Build Tools 버전 35.0.0 이상과 JDK 17이 필요합니다.

수정 사항:

  • 버전을 8.13.0으로 변경하거나, 더 최신 안정 버전(8.11 또는 8.12)의 존재 여부를 프로젝트 요구 사항에 맞춰 확인하세요.
  • 현재까지 AGP 8.13.x 시리즈에 대해 알려진 보안 취약점은 없습니다.

Likely an incorrect or invalid review comment.

app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt (1)

8-20: LGTM! 수동 주입(App Container) 구현이 올바릅니다.

AppContainer가 의존성을 중앙에서 관리하고, lazy 초기화를 통해 필요할 때만 객체를 생성하는 방식이 적절합니다. apiService를 private으로 감추고 postRepository만 노출하는 캡슐화도 좋습니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt (1)

49-71: LGTM! PostDetailViewModel 마이그레이션이 올바르게 적용되었습니다.

팩토리 기반 ViewModel 주입과 uiState 패턴이 정확히 구현되었고, LaunchedEffect에서 postId를 키로 사용하여 ID 변경 시 데이터를 다시 가져오는 로직도 적절합니다.

app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (1)

5-86: LGTM! 네비게이션 그래프에서 팩토리 기반 ViewModel 주입이 일관되게 적용되었습니다.

네 개의 화면 모두에 postViewModelFactory를 통해 각각의 전용 ViewModel을 주입하는 패턴이 정확하고 일관성 있게 구현되어 있습니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt (1)

56-284: LGTM! PostCreateViewModel 마이그레이션과 uiState 패턴이 일관되게 적용되었습니다.

uiState.isUploadinguiState.uploadedImageUrl를 일관되게 사용하여 UI 상태를 관리하고 있으며, 팩토리 기반 주입도 정확히 구현되었습니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt (1)

55-258: LGTM! PostEditViewModel 마이그레이션과 uiState 패턴이 정확히 구현되었습니다.

uiState를 통해 게시글 상세 정보, 업로드 상태, 이미지 URL을 일관되게 관리하고 있으며, 팩토리 기반 주입도 올바르게 적용되었습니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt (1)

12-83: LGTM! PostListViewModel 마이그레이션과 UiState 패턴, 그리고 사이드 이펙트 구현이 완벽합니다.

StateFlowcollectAsState()로 수집하고, LaunchedEffect(Unit)에서 refresh()를 호출하여 미션 요구사항(메인화면에서 refresh를 side effect로 실행)을 정확히 구현했습니다. when(uiState) 블록으로 Loading, Success, Error 상태를 모두 처리하는 것도 깔끔합니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt (1)

1-36: LGTM! PostListViewModel 구현이 깔끔하고 올바릅니다.

Repository 주입, StateFlow 패턴, viewModelScope를 활용한 비동기 처리, 그리고 ResultonSuccess/onFailure를 사용한 에러 처리가 모두 적절하게 구현되어 있습니다. refresh() 함수가 loadPosts()를 호출하는 구조도 명확합니다.

Comment on lines +26 to +96
override suspend fun getPostDetail(postId: Long): Result<PostResponse> {
return runCatching {
val response: BaseResponse<PostResponse> = apiService.getPostDetail(postId)
if (response.success && response.data != null) {
response.data
} else {
throw Exception(response.message ?: "게시글 상세 조회 실패")
}
}
}

override suspend fun createPost(
author: String,
title: String,
content: String,
imageUrl: String?
): Result<PostResponse> {
return runCatching {
val request = PostCreateRequest(title, content, imageUrl)
val response: BaseResponse<PostResponse> = apiService.createPost(author, request)
if (response.success && response.data != null) {
response.data
} else {
throw Exception(response.message ?: "게시글 생성 실패")
}
}
}

override suspend fun updatePost(
postId: Long,
title: String,
content: String,
imageUrl: String?
): Result<PostResponse> {
return runCatching {
val request = PostCreateRequest(title, content, imageUrl)
val response: BaseResponse<PostResponse> = apiService.updatePost(postId, request)
if (response.success && response.data != null) {
response.data
} else {
throw Exception(response.message ?: "게시글 수정 실패")
}
}
}

override suspend fun deletePost(postId: Long): Result<Unit> {
return runCatching {
val response: BaseResponse<Unit> = apiService.deletePost(postId)
if (response.success) {
Unit
} else {
throw Exception(response.message ?: "게시글 삭제 실패")
}
}
}

override suspend fun uploadImage(file: MultipartBody.Part): Result<String> {
return runCatching {
val response: BaseResponse<Map<String, String>> = apiService.uploadImage(file)
if (response.success && response.data != null) {
val imageUrl = response.data["imageUrl"]
if (imageUrl != null) {
imageUrl
} else {
throw Exception("이미지 URL을 받아오지 못했습니다")
}
} else {
throw Exception(response.message ?: "이미지 업로드 실패")
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

에러 로깅을 일관되게 적용하세요.

getPosts 메서드만 .onFailure에서 에러를 로깅하고 있으며, 다른 메서드들(getPostDetail, createPost, updatePost, deletePost, uploadImage)은 에러 로깅이 누락되어 있습니다. 디버깅과 모니터링을 위해 모든 메서드에 일관된 에러 로깅을 적용하는 것을 권장합니다.

각 메서드에 다음과 같이 에러 로깅을 추가하세요:

 override suspend fun getPostDetail(postId: Long): Result<PostResponse> {
     return runCatching {
         val response: BaseResponse<PostResponse> = apiService.getPostDetail(postId)
         if (response.success && response.data != null) {
             response.data
         } else {
             throw Exception(response.message ?: "게시글 상세 조회 실패")
         }
-    }
+    }.onFailure { error ->
+        Log.e("PostRepository", error.message.toString())
+    }
 }

나머지 메서드들(createPost, updatePost, deletePost, uploadImage)에도 동일한 패턴을 적용하세요.

📝 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.

Suggested change
override suspend fun getPostDetail(postId: Long): Result<PostResponse> {
return runCatching {
val response: BaseResponse<PostResponse> = apiService.getPostDetail(postId)
if (response.success && response.data != null) {
response.data
} else {
throw Exception(response.message ?: "게시글 상세 조회 실패")
}
}
}
override suspend fun createPost(
author: String,
title: String,
content: String,
imageUrl: String?
): Result<PostResponse> {
return runCatching {
val request = PostCreateRequest(title, content, imageUrl)
val response: BaseResponse<PostResponse> = apiService.createPost(author, request)
if (response.success && response.data != null) {
response.data
} else {
throw Exception(response.message ?: "게시글 생성 실패")
}
}
}
override suspend fun updatePost(
postId: Long,
title: String,
content: String,
imageUrl: String?
): Result<PostResponse> {
return runCatching {
val request = PostCreateRequest(title, content, imageUrl)
val response: BaseResponse<PostResponse> = apiService.updatePost(postId, request)
if (response.success && response.data != null) {
response.data
} else {
throw Exception(response.message ?: "게시글 수정 실패")
}
}
}
override suspend fun deletePost(postId: Long): Result<Unit> {
return runCatching {
val response: BaseResponse<Unit> = apiService.deletePost(postId)
if (response.success) {
Unit
} else {
throw Exception(response.message ?: "게시글 삭제 실패")
}
}
}
override suspend fun uploadImage(file: MultipartBody.Part): Result<String> {
return runCatching {
val response: BaseResponse<Map<String, String>> = apiService.uploadImage(file)
if (response.success && response.data != null) {
val imageUrl = response.data["imageUrl"]
if (imageUrl != null) {
imageUrl
} else {
throw Exception("이미지 URL을 받아오지 못했습니다")
}
} else {
throw Exception(response.message ?: "이미지 업로드 실패")
}
}
}
override suspend fun getPostDetail(postId: Long): Result<PostResponse> {
return runCatching {
val response: BaseResponse<PostResponse> = apiService.getPostDetail(postId)
if (response.success && response.data != null) {
response.data
} else {
throw Exception(response.message ?: "게시글 상세 조회 실패")
}
}.onFailure { error ->
Log.e("PostRepository", error.message.toString())
}
}
override suspend fun createPost(
author: String,
title: String,
content: String,
imageUrl: String?
): Result<PostResponse> {
return runCatching {
val request = PostCreateRequest(title, content, imageUrl)
val response: BaseResponse<PostResponse> = apiService.createPost(author, request)
if (response.success && response.data != null) {
response.data
} else {
throw Exception(response.message ?: "게시글 생성 실패")
}
}
}
override suspend fun updatePost(
postId: Long,
title: String,
content: String,
imageUrl: String?
): Result<PostResponse> {
return runCatching {
val request = PostCreateRequest(title, content, imageUrl)
val response: BaseResponse<PostResponse> = apiService.updatePost(postId, request)
if (response.success && response.data != null) {
response.data
} else {
throw Exception(response.message ?: "게시글 수정 실패")
}
}
}
override suspend fun deletePost(postId: Long): Result<Unit> {
return runCatching {
val response: BaseResponse<Unit> = apiService.deletePost(postId)
if (response.success) {
Unit
} else {
throw Exception(response.message ?: "게시글 삭제 실패")
}
}
}
override suspend fun uploadImage(file: MultipartBody.Part): Result<String> {
return runCatching {
val response: BaseResponse<Map<String, String>> = apiService.uploadImage(file)
if (response.success && response.data != null) {
val imageUrl = response.data["imageUrl"]
if (imageUrl != null) {
imageUrl
} else {
throw Exception("이미지 URL을 받아오지 못했습니다")
}
} else {
throw Exception(response.message ?: "이미지 업로드 실패")
}
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt
around lines 26 to 96, several suspend methods (getPostDetail, createPost,
updatePost, deletePost, uploadImage) wrap API calls in runCatching but do not
attach error logging like getPosts does; for each of these methods, chain
.onFailure { /* use the same logger used by getPosts, e.g.
logger.error("MethodName failed", it) or Timber.e(it, "MethodName failed") */ }
to the Result returned by runCatching so the exception is logged with a clear
message and the throwable, keeping the same logging pattern and message style as
getPosts.

private val repository: PostRepository
// 의존성 주입:뷰모델의 파라미터로 Repository를 전달하는 것
) : ViewModel() {
//st
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

불완전한 주석을 제거하거나 완성하세요.

Line 27의 //st 주석이 불완전해 보입니다. 의도한 내용이 있다면 완성하고, 그렇지 않다면 제거하세요.

-    //st
     var uiState by mutableStateOf(PostCreateUiState())
         private set
📝 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.

Suggested change
//st
var uiState by mutableStateOf(PostCreateUiState())
private set
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt
around line 27, there is an incomplete comment "//st"; either remove this stray
comment or replace it with a meaningful, complete comment that explains the
following code's intent (e.g., purpose of the class/variable/method) to keep the
codebase clean and readable.

Comment on lines +31 to +45
fun createPost(
author: String,
title: String,
content: String,
imageUrl: String? = null,
onSuccess: () -> Unit = {}
) {
viewModelScope.launch {
repository.createPost(author, title, content, imageUrl)
.onSuccess {
uiState = uiState.copy(uploadedImageUrl = null)
onSuccess()
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

생성 실패 시 에러 처리가 누락되었습니다.

createPost가 성공 케이스만 처리하고 있어, 게시글 생성이 실패해도 사용자가 알 수 없습니다.

다음과 같이 실패 처리를 추가하세요:

 fun createPost(
     author: String,
     title: String,
     content: String,
     imageUrl: String? = null,
     onSuccess: () -> Unit = {}
 ) {
     viewModelScope.launch {
         repository.createPost(author, title, content, imageUrl)
             .onSuccess {
                 uiState = uiState.copy(uploadedImageUrl = null)
                 onSuccess()
             }
+            .onFailure { error ->
+                Log.e("PostCreateViewModel", "게시글 생성 실패: ${error.message}")
+                // 에러 콜백을 추가하거나 UI 상태로 에러를 노출
+            }
     }
 }
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt
around lines 31 to 45, the createPost function currently only handles the
success case, so failures are silent; add failure handling by attaching an
onFailure (or equivalent error callback) to repository.createPost inside the
viewModelScope.launch, update uiState to reflect the error (e.g., set an error
message field or a flag and clear uploadedImageUrl if needed), call any provided
error callback or surface the error to the UI (e.g., via a LiveData/State
property), and log the exception for debugging; ensure the coroutine catches
unexpected exceptions as well (try/catch) and maps them into the same uiState
error handling.

Comment on lines +22 to +32
fun getPostDetail(postId: Long) {
viewModelScope.launch {
repository.getPostDetail(postId)
.onSuccess { post ->
uiState = uiState.copy(postDetail = post)
}
.onFailure {
uiState = uiState.copy(postDetail = null)
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

실패 시 에러 처리 및 로깅이 필요합니다.

onFailure 블록에서 에러를 무시하고 있어 사용자가 로딩 실패 원인을 알 수 없습니다. 최소한 로그를 남기거나 UI 상태에 에러 메시지를 노출하는 것을 권장합니다.

다음과 같이 에러 로깅을 추가하는 것을 고려하세요:

 fun getPostDetail(postId: Long) {
     viewModelScope.launch {
         repository.getPostDetail(postId)
             .onSuccess { post ->
                 uiState = uiState.copy(postDetail = post)
             }
             .onFailure {
+                Log.e("PostDetailViewModel", "게시글 상세 조회 실패: ${it.message}")
                 uiState = uiState.copy(postDetail = null)
             }
     }
 }

또는 UiState에 error 필드를 추가하여 사용자에게 에러를 표시할 수 있습니다.

📝 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.

Suggested change
fun getPostDetail(postId: Long) {
viewModelScope.launch {
repository.getPostDetail(postId)
.onSuccess { post ->
uiState = uiState.copy(postDetail = post)
}
.onFailure {
uiState = uiState.copy(postDetail = null)
}
}
}
fun getPostDetail(postId: Long) {
viewModelScope.launch {
repository.getPostDetail(postId)
.onSuccess { post ->
uiState = uiState.copy(postDetail = post)
}
.onFailure {
Log.e("PostDetailViewModel", "게시글 상세 조회 실패: ${it.message}")
uiState = uiState.copy(postDetail = null)
}
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt
around lines 22 to 32, the onFailure branch currently swallows the error; change
it to log the Throwable and surface the error to the UI by updating UiState:
capture the Throwable provided to onFailure, call your logger (e.g., Timber/Log)
to record the message/stacktrace, and set a new error field on uiState (add an
error: String? or Throwable? to the UiState data class) so the UI can display a
user-friendly message; ensure you still clear or keep postDetail as appropriate
and reset the error on successful fetch.

Comment on lines +34 to +41
fun deletePost(postId: Long, onSuccess: () -> Unit = {}) {
viewModelScope.launch {
repository.deletePost(postId)
.onSuccess {
onSuccess()
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

삭제 실패 시 에러 처리가 누락되었습니다.

deletePost 함수가 성공 케이스만 처리하고 실패 케이스를 처리하지 않아, 삭제가 실패해도 사용자가 알 수 없습니다.

다음과 같이 실패 처리를 추가하세요:

 fun deletePost(postId: Long, onSuccess: () -> Unit = {}) {
     viewModelScope.launch {
         repository.deletePost(postId)
             .onSuccess {
                 onSuccess()
             }
+            .onFailure { error ->
+                Log.e("PostDetailViewModel", "게시글 삭제 실패: ${error.message}")
+                // 에러 콜백을 추가하거나 UI 상태로 에러를 노출
+            }
     }
 }
📝 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.

Suggested change
fun deletePost(postId: Long, onSuccess: () -> Unit = {}) {
viewModelScope.launch {
repository.deletePost(postId)
.onSuccess {
onSuccess()
}
}
}
fun deletePost(postId: Long, onSuccess: () -> Unit = {}) {
viewModelScope.launch {
repository.deletePost(postId)
.onSuccess {
onSuccess()
}
.onFailure { error ->
Log.e("PostDetailViewModel", "게시글 삭제 실패: ${error.message}")
// 에러 콜백을 추가하거나 UI 상태로 에러를 노출
}
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt
around lines 34-41, the deletePost function only handles the success case and
omits failure handling; add an onFailure handler (or a try/catch around the
repository call) to handle errors returned from repository.deletePost(postId).
Inside the failure branch, log the error, update the ViewModel's error state
(LiveData/StateFlow) or invoke a provided error callback so the UI can show an
error message, and avoid swallowing exceptions silently.

Comment on lines +30 to +40
fun getPostDetail(postId: Long) {
viewModelScope.launch {
repository.getPostDetail(postId)
.onSuccess { post ->
uiState = uiState.copy(postDetail = post)
}
.onFailure {
uiState = uiState.copy(postDetail = null)
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

에러 처리 및 코드 중복 문제가 있습니다.

  1. onFailure 블록에서 에러를 로깅하지 않아 디버깅이 어렵습니다.
  2. getPostDetail 구현은 PostDetailViewModel의 것과 동일하여 코드 중복이 발생합니다.

에러 로깅 추가:

 fun getPostDetail(postId: Long) {
     viewModelScope.launch {
         repository.getPostDetail(postId)
             .onSuccess { post ->
                 uiState = uiState.copy(postDetail = post)
             }
             .onFailure {
+                Log.e("PostEditViewModel", "게시글 상세 조회 실패: ${it.message}")
                 uiState = uiState.copy(postDetail = null)
             }
     }
 }

코드 중복 해결 고려사항:
PostDetailViewModelPostEditViewModel이 동일한 getPostDetail 로직을 사용하고 있습니다. 공통 기능을 base ViewModel로 추출하거나 helper 함수로 분리하는 것을 고려하세요.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +42 to +56
fun updatePost(
postId: Long,
title: String,
content: String,
imageUrl: String? = null,
onSuccess: () -> Unit = {}
) {
viewModelScope.launch {
repository.updatePost(postId, title, content, imageUrl)
.onSuccess {
uiState = uiState.copy(uploadedImageUrl = null)
onSuccess()
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

수정 실패 시 에러 처리가 누락되었습니다.

updatePost가 성공 케이스만 처리하고 있어, 게시글 수정이 실패해도 사용자가 알 수 없습니다.

다음과 같이 실패 처리를 추가하세요:

 fun updatePost(
     postId: Long,
     title: String,
     content: String,
     imageUrl: String? = null,
     onSuccess: () -> Unit = {}
 ) {
     viewModelScope.launch {
         repository.updatePost(postId, title, content, imageUrl)
             .onSuccess {
                 uiState = uiState.copy(uploadedImageUrl = null)
                 onSuccess()
             }
+            .onFailure { error ->
+                Log.e("PostEditViewModel", "게시글 수정 실패: ${error.message}")
+                // 에러 콜백을 추가하거나 UI 상태로 에러를 노출
+            }
     }
 }
📝 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.

Suggested change
fun updatePost(
postId: Long,
title: String,
content: String,
imageUrl: String? = null,
onSuccess: () -> Unit = {}
) {
viewModelScope.launch {
repository.updatePost(postId, title, content, imageUrl)
.onSuccess {
uiState = uiState.copy(uploadedImageUrl = null)
onSuccess()
}
}
}
fun updatePost(
postId: Long,
title: String,
content: String,
imageUrl: String? = null,
onSuccess: () -> Unit = {}
) {
viewModelScope.launch {
repository.updatePost(postId, title, content, imageUrl)
.onSuccess {
uiState = uiState.copy(uploadedImageUrl = null)
onSuccess()
}
.onFailure { error ->
Log.e("PostEditViewModel", "게시글 수정 실패: ${error.message}")
// 에러 콜백을 추가하거나 UI 상태로 에러를 노출
}
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt
around lines 42 to 56, updatePost only handles the success branch and omits
failure handling; add error handling so failures surface to the UI: catch
repository update failures (either via repository's Result.onFailure or a
try/catch around the suspend call), update uiState with an error message/flag
(and clear any transient fields like uploadedImageUrl if appropriate), log the
error, and expose an optional onError callback parameter to notify the caller so
the UI can show an error toast/dialog.

Comment on lines +62 to +93
fun uploadImage(
context: Context,
uri: Uri,
onSuccess: (String) -> Unit = {},
onError: (String) -> Unit = {}
) {
viewModelScope.launch {
uiState = uiState.copy(isUploading = true)
val file = UriUtils.uriToFile(context, uri)
if (file == null) {
uiState = uiState.copy(isUploading = false)
onError("파일 변환 실패")
return@launch
}

val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)

repository.uploadImage(body)
.onSuccess { imageUrl ->
uiState = uiState.copy(
isUploading = false,
uploadedImageUrl = imageUrl
)
onSuccess(imageUrl)
}
.onFailure { error ->
uiState = uiState.copy(isUploading = false)
onError(error.message ?: "업로드 실패")
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

코드 중복: uploadImage 로직이 중복되었습니다.

uploadImage 구현은 PostCreateViewModel의 것과 거의 동일합니다(lines 51-82). 두 ViewModel에서 동일한 로직을 유지 관리하면 버그 발생 가능성이 높아집니다.

다음 중 하나의 방법으로 중복을 제거하세요:

옵션 1: Base ViewModel 사용

abstract class BasePostViewModel(
    protected val repository: PostRepository
) : ViewModel() {
    
    protected fun uploadImageInternal(
        context: Context,
        uri: Uri,
        onUploadingChange: (Boolean) -> Unit,
        onSuccess: (String) -> Unit,
        onError: (String) -> Unit
    ) {
        viewModelScope.launch {
            onUploadingChange(true)
            val file = UriUtils.uriToFile(context, uri)
            if (file == null) {
                onUploadingChange(false)
                onError("파일 변환 실패")
                return@launch
            }
            
            val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
            val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
            
            repository.uploadImage(body)
                .onSuccess { imageUrl ->
                    onUploadingChange(false)
                    onSuccess(imageUrl)
                }
                .onFailure { error ->
                    onUploadingChange(false)
                    onError(error.message ?: "업로드 실패")
                }
        }
    }
}

옵션 2: 별도의 ImageUploadHelper 클래스 생성

class ImageUploadHelper(
    private val repository: PostRepository,
    private val scope: CoroutineScope
) {
    fun uploadImage(
        context: Context,
        uri: Uri,
        onUploadingChange: (Boolean) -> Unit,
        onSuccess: (String) -> Unit,
        onError: (String) -> Unit
    ) { /* 구현 */ }
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt
around lines 62-93, the uploadImage logic is duplicated with
PostCreateViewModel; extract the shared upload flow into a single reusable unit
(either a protected uploadImageInternal in a new BasePostViewModel that both
viewmodels extend, or into a standalone ImageUploadHelper injected with the
repository and coroutine scope). Move the coroutine, file conversion,
requestBody creation, repository.uploadImage call and success/failure callbacks
into that shared method and expose callbacks for uploading-state, success
(imageUrl) and error messages; then replace the duplicated code in both
viewmodels with calls to the shared method, wiring uiState updates through the
provided callbacks and ensuring the correct viewModelScope (or injected scope)
is used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants