Skip to content

7주차 미션 / 안드로이드 2조 사예원#22

Open
BearYeWon wants to merge 12 commits into
Konkuk-KUIT:BearYeWonfrom
BearYeWon:BearYeWon/week7
Open

7주차 미션 / 안드로이드 2조 사예원#22
BearYeWon wants to merge 12 commits into
Konkuk-KUIT:BearYeWonfrom
BearYeWon:BearYeWon/week7

Conversation

@BearYeWon

@BearYeWon BearYeWon commented Nov 13, 2025

Copy link
Copy Markdown

📝 미션

미션: PostViewModel 각 screen에 맞는 ViewModel로 분리

🙋 구현에 대한 설명

  • 수동 주입(App Container) 구현
  • Repository 패턴 사용
  • ViewModel 분리
    • PostEditViewModel
    • PostDetailViewModel (삭제까지 같이 하시면 됩니다)
    • PostCreateViewModel
  • UiState 구현
  • PostViewModel 삭제
  • PostListViewModel의 refresh 함수를 메인화면에서 sideEffect로 항상 실행시키기

미션 구현 설명

PostRepositoryImpl

  • PostRepository 인터페이스에서 정의한 suspend 함수 구현

UiState

  • DeletePostUiState, PostCreateUiState, PostDetailUiState, PostEditUiState, UploadImageUiState -> 각 상태 정의 (UploadImageUiState는 업로드 중이 아닌데도 '업로드 중...' 표시 -> Idle 상태 추가)

ViewModel

  • PostCreateViewModel : createPost(), uploadImage(), clearUploadedImageUrl()
  • PostDetailViewModel : loadDetail(), deletePost()
  • PostEditViewModel : editPost(), uploadImage(), clearUploadedImageUrl(), getPostDetail()
  • PostViewModel : 삭제

Screen

  • 각 스크린에서 인자로 받은 뷰모델 state 구독
  • 상태에 따른 동작 분기 처리
  • PostListScreen : LauncedEffect() 사용해서 새로고침 (바로 casting하면 오류 -> posts 변수 선언 후 사용)

📷 스크린샷 & 실행영상

7.mp4

🎸 기타

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 게시물 생성 및 수정 시 이미지 업로드 기능 추가
    • 실시간 API 연동으로 더욱 안정적인 게시물 관리 기능 제공
  • Refactor

    • 앱 아키텍처 개선 및 상태 관리 시스템 최적화로 더욱 안정적인 사용자 경험 제공

@coderabbitai

coderabbitai Bot commented Nov 13, 2025

Copy link
Copy Markdown

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

API 통신 인프라를 구축하고 의존성 주입 패턴을 도입했습니다. Retrofit과 OkHttp를 설정하고, PostRepository 기반의 데이터 계층을 추가했으며, 기존 PostViewModel을 네 개의 특화된 ViewModel로 분리하고 UI 상태 클래스들을 도입했습니다. 구식 Post/Author 모델을 제거하고 API 응답 기반의 새로운 모델로 대체했습니다.

Changes

Cohort / File(s) 변경 사항
API 인프라
app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt, app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt
Retrofit 기반 API 서비스 인터페이스 추가, HTTP 클라이언트 및 로깅 인터셉터 설정
데이터 모델 - 응답
app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt, PostResponse.kt, AuthResponse.kt, ImageUploadResponse.kt
제네릭 BaseResponse 래퍼 및 API 응답 모델들 추가
데이터 모델 - 요청
app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt
포스트 생성 요청 모델 추가
제거된 모델
app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt
구식 Post/Author 모델 제거
저장소 패턴
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt, PostRepositoryImpl.kt
PostRepository 인터페이스 및 구현체 추가
의존성 주입
app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt, App.kt, app/src/main/AndroidManifest.xml
AppContainer 및 App 클래스 추가, 애플리케이션 초기화
UI 상태 관리
app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt, PostDetailUiState.kt, PostCreateUiState.kt, PostEditUiState.kt, DeletePostUiState.kt, UploadImageUiState.kt
UI 상태를 나타내는 sealed 클래스들 추가
ViewModel - 신규
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt, PostDetailViewModel.kt, PostCreateViewModel.kt, PostEditViewModel.kt, PostViewModelFactory.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/ui/post/viewmodel/UriUtils.kt
URI를 파일로 변환하는 유틸리티 함수 추가
UI 화면 업데이트
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt, PostDetailScreen.kt, PostCreateScreen.kt, PostEditScreen.kt
ViewModel 타입 변경, UI 상태 기반 렌더링, 이미지 업로드 기능 추가
UI 컴포넌트
app/src/main/java/com/example/kuit6_android_api/ui/post/component/PostItem.kt
PostResponse 모델로 타입 변경
네비게이션
app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt
ViewModel 주입 및 상태 동기화 로직 추가
빌드 설정
app/build.gradle.kts, gradle/libs.versions.toml
BuildConfig BASE_URL 로드, Retrofit/OkHttp 의존성 추가, 직렬화 컨버터 라이브러리 추가
IDE 설정
.idea/deploymentTargetSelector.xml, .idea/gradle.xml, CLAUDE.md
배포 대상 선택기 타임스탬프 업데이트, Gradle 모듈 경로 수정, 문서 제거

Sequence Diagram(s)

sequenceDiagram
    participant Screen as UI Screen
    participant ViewModel as ViewModel
    participant Repo as Repository
    participant ApiService as ApiService
    participant Network as Network

    Note over Screen,Network: 포스트 목록 로드 흐름
    Screen->>ViewModel: refresh()
    ViewModel->>ViewModel: postListUiState = Loading
    ViewModel->>Repo: getPosts()
    Repo->>ApiService: getPosts()
    ApiService->>Network: GET /api/posts
    Network-->>ApiService: BaseResponse<List<PostResponse>>
    ApiService-->>Repo: BaseResponse<List<PostResponse>>
    rect rgb(200, 220, 255)
        Note over Repo: 응답 검증<br/>(success, data)
    end
    Repo-->>ViewModel: Result<List<PostResponse>>
    ViewModel->>ViewModel: postListUiState = Success(posts)
    ViewModel-->>Screen: collectAsState() → Success
    Screen->>Screen: render LazyColumn
Loading
sequenceDiagram
    participant Screen as PostCreateScreen
    participant ViewModel as PostCreateViewModel
    participant UriUtils as UriUtils
    participant Repo as Repository
    participant ApiService as ApiService

    Note over Screen,ApiService: 이미지 업로드 흐름
    Screen->>UriUtils: uriToFile(uri)
    UriUtils-->>Screen: File
    Screen->>Screen: MultipartBody.Part
    Screen->>ViewModel: uploadImage(file)
    ViewModel->>ViewModel: uploadImageUiState = Loading
    ViewModel->>Repo: uploadImage(file)
    Repo->>ApiService: uploadImage(file)
    ApiService-->>Repo: BaseResponse<Map<String,String>>
    rect rgb(220, 255, 220)
        Note over Repo: 성공 케이스
    end
    Repo-->>ViewModel: Result<Map<String,String>>
    ViewModel->>ViewModel: uploadImageUiState = Success(data)
    Screen->>Screen: imageUrl 저장 및 미리보기
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 분

  • 특히 주의가 필요한 부분:
    • PostRepositoryImpl.kt: runCatching과 Result 타입 처리 로직에서 에러 메시지 매핑 검증
    • PostCreateScreen.kt: 이미지 업로드 상태 관리와 MultipartBody.Part 변환 로직의 정확성
    • PostDetailScreen.kt: UI 상태 기반 렌더링에서 null 안전성 확인 (Success.post 접근)
    • PostListViewModel.kt: 초기 로드 시점과 refresh() 호출의 중복 방지
    • NavGraph.kt: ViewModel 팩토리 주입과 상태 동기화 (savedStateHandle 플래그) 로직
    • AppContainer.ktApp.kt: 의존성 주입 초기화 순서 및 스코프 관리

Possibly related PRs

  • PR #16: ApiService, RetrofitClient, BaseResponse/PostResponse/PostCreateRequest 모델, 빌드 설정 변경이 동일하게 적용되었습니다.
  • PR #17: Retrofit/ApiService 구현, 저장소/ViewModel, UriUtils, 네비게이션 통합, 빌드 설정, PostViewModel 제거 등 거의 동일한 변경 사항입니다.
  • PR #18: app/build.gradle.kts, API 서비스, 응답/요청 모델, PostItem 타입 변경 등 공통된 변경 사항입니다.

Poem

🐰 네트워크 길을 따라, API 호출하네~
의존성 담아 컨테이너에 담아,
포스트를 그려내고, 이미지 올려~
ViewModel의 춤, 상태가 흐르네.
안녕 PostViewModel, 새로운 친구들 환영해! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive PR 제목은 미션명과 조원명을 포함하지만, 변경사항의 핵심 내용을 명확하게 전달하지 못합니다. 제목을 'PostViewModel 분리: 각 화면별 전담 ViewModel 구현' 또는 'Repository 패턴 및 의존성 주입 적용' 등으로 변경하여 주요 변경사항을 더 명확하게 표현하는 것을 권장합니다.
✅ 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.

@BearYeWon BearYeWon closed this Nov 13, 2025
@BearYeWon BearYeWon reopened this Nov 13, 2025
@alswlekk

Copy link
Copy Markdown
Contributor

📝 미션

미션: PostViewModel 각 screen에 맞는 ViewModel로 분리

🙋 구현에 대한 설명

  • 수동 주입(App Container) 구현

  • Repository 패턴 사용

  • ViewModel 분리

    • PostEditViewModel
    • PostDetailViewModel (삭제까지 같이 하시면 됩니다)
    • PostCreateViewModel
  • UiState 구현

  • PostViewModel 삭제

  • PostListViewModel의 refresh 함수를 메인화면에서 sideEffect로 항상 실행시키기

미션 구현 설명

PostRepositoryImpl

  • PostRepository 인터페이스에서 정의한 suspend 함수 구현

UiState

  • DeletePostUiState, PostCreateUiState, PostDetailUiState, PostEditUiState, UploadImageUiState -> 각 상태 정의 (UploadImageUiState는 업로드 중이 아닌데도 '업로드 중...' 표시 -> Idle 상태 추가)

ViewModel

  • PostCreateViewModel : createPost(), uploadImage(), clearUploadedImageUrl()
  • PostDetailViewModel : loadDetail(), deletePost()
  • PostEditViewModel : editPost(), uploadImage(), clearUploadedImageUrl(), getPostDetail()
  • PostViewModel : 삭제

Screen

  • 각 스크린에서 인자로 받은 뷰모델 state 구독
  • 상태에 따른 동작 분기 처리
  • PostListScreen : LauncedEffect() 사용해서 새로고침 (바로 casting하면 오류 -> posts 변수 선언 후 사용)

📷 스크린샷 & 실행영상

7.mp4

🎸 기타

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 14, 2025

Copy link
Copy Markdown

@alswlekk 안녕하세요! PR 리뷰를 시작하겠습니다.

미션 구현 내용을 상세히 정리해 주셔서 감사합니다. PostViewModel을 각 화면별로 분리하고, Repository 패턴과 UiState를 구현하신 작업을 검토하겠습니다. 곧 자세한 리뷰 결과를 제공해 드리겠습니다.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (17)
.idea/gradle.xml (1)

12-13: .idea/gradle.xml 변경은 가능하면 VCS에서 제외하는 것도 고려해 보세요.

이 파일은 IDE 로컬 설정 성격이 강해서, 팀원별 환경 차이로 인해 자주 변경될 수 있습니다. 현재 변경 자체는 문제 없지만, 팀 컨벤션에 따라 .idea/gradle.xml 을 커밋 대상에서 제외하거나 최소한으로만 관리할지 한 번 논의해 보셔도 좋겠습니다.

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

6-12: lateinit var 대신 lazy 초기화 고려

현재 lateinit var 패턴은 일반적이지만, onCreate() 전에 접근 시 크래시가 발생할 수 있습니다. 더 안전한 대안으로 lazy 초기화를 고려해보세요.

다음과 같이 리팩토링할 수 있습니다:

-    lateinit var container: AppContainer
-
-    override fun onCreate() {
-        super.onCreate()
-        container = AppContainer()
-    }
+    val container: AppContainer by lazy {
+        AppContainer()
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        // container를 초기화하려면 여기서 접근
+        container
+    }
app/src/main/java/com/example/kuit6_android_api/ui/post/state/UploadImageUiState.kt (1)

7-9: Success 상태의 필드명 개선 제안

imgUrl 필드명은 단일 URL을 암시하지만 실제로는 Map<String, String> 타입입니다. 더 명확한 이름(예: imageData 또는 uploadResponse)을 사용하면 의도가 더 명확해집니다.

     data class Success(
-        val imgUrl: Map<String, String>
+        val imageData: Map<String, String>
     ) : UploadImageUiState()
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt (2)

105-119: 타입 캐스팅을 안전하게 개선하세요.

when 표현식 내부에서 이미 타입 체크를 했음에도 불구하고 명시적 캐스팅(as)을 사용하고 있습니다. Kotlin의 스마트 캐스트를 활용하면 더 안전하고 간결한 코드가 됩니다.

다음과 같이 수정하세요:

 LaunchedEffect(imgUiState) {
     when (imgUiState) {
         is UploadImageUiState.Success -> {
-            uploadedImageUrl = (imgUiState as UploadImageUiState.Success).imgUrl["imageUrl"]
+            uploadedImageUrl = imgUiState.imgUrl["imageUrl"]
         }
         is UploadImageUiState.Error -> {
             Toast.makeText(
                 context,
-                (imgUiState as UploadImageUiState.Error).message,
+                imgUiState.message,
                 Toast.LENGTH_SHORT
             ).show()
         }
         else -> Unit
     }
 }

151-158: 타입 캐스팅을 제거하고 스마트 캐스트를 활용하세요.

when 분기 내에서 불필요한 명시적 캐스팅을 사용하고 있습니다.

 is PostEditUiState.Success -> {
-    val post = (uiState as PostEditUiState.Success).post
+    val post = uiState.post
 
     if (!isLoaded) {
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/UriUtils.kt (1)

22-25: 로깅 개선을 고려해보세요.

현재 printStackTrace()만 호출하고 있는데, Android의 Log 클래스를 사용하면 더 체계적인 로깅이 가능합니다.

+import android.util.Log
+
 object UriUtils {
+    private const val TAG = "UriUtils"
+
     fun uriToFile(context: Context, uri: Uri): File? {
         return try {
             // ...
         } catch (e: Exception) {
-            e.printStackTrace()
+            Log.e(TAG, "Failed to convert URI to File: $uri", e)
             null
         }
     }
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt (2)

17-17: 초기 상태를 Loading으로 설정하는 것을 재고하세요.

ViewModel이 생성될 때 아직 데이터 로딩이 시작되지 않았는데도 Loading 상태로 초기화되어 있습니다. 다른 ViewModel들(예: PostEditViewModel)처럼 일관성을 유지하기 위해 현재 상태를 유지하거나, 별도의 Idle 상태를 추가하는 것을 고려해보세요.


43-57: 변수명을 더 명확하게 수정하세요.

Line 48의 deletePost 변수명이 함수명과 동일하여 혼란을 줄 수 있습니다.

 fun deletePost(postId: Long){
     viewModelScope.launch {
         _deleteUiState.value = DeletePostUiState.Loading
 
         postRepository.deletePost(postId)
-            .onSuccess { deletePost ->
-                _deleteUiState.value = DeletePostUiState.Success(deletePost)
+            .onSuccess {
+                _deleteUiState.value = DeletePostUiState.Success
             }
             .onFailure { error ->
                 _deleteUiState.value = DeletePostUiState.Error(
                     error.message?: "error"
                 )
             }
     }
 }

참고: 이 제안은 DeletePostUiState.Successdata object로 변경된다는 가정 하에 작성되었습니다.

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

19-19: 초기 상태 설정을 재검토하세요.

PostDetailViewModel과 마찬가지로 ViewModel 생성 시점에 아직 로딩이 시작되지 않았는데도 Loading 상태로 초기화되어 있습니다. 일관성을 위해 다른 상태(예: 별도의 Idle)로 초기화하는 것을 고려해보세요.

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

12-40: StateFlow 백킹 필드 네이밍을 Kotlin 관례에 맞게 정리하면 좋겠습니다

기능적으로는 문제 없지만, _PostList_uiState 네이밍은 Pascal/snake가 섞여 있어서 Kotlin 관례(소문자 카멜 + _ 접두어)와 조금 어긋납니다.

예를 들어 아래처럼 정리하면 코드 읽기가 더 수월합니다.

private val _postListUiState = MutableStateFlow<PostListUiState>(PostListUiState.Loading)
val postListUiState: StateFlow<PostListUiState> = _postListUiState.asStateFlow()
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt (1)

39-42: 로딩/에러 레이아웃 padding 처리 및 초기 이중 로딩 가능성 점검 제안

두 가지 정도만 한 번 더 봐주시면 좋겠습니다.

  1. Loading/Error 분기에서 Scaffold padding 미적용
when (uiState) {
    is PostListUiState.Loading -> {
        CircularProgressIndicator()
    }
    ...
    is PostListUiState.Error -> {
        Text("로딩 실패")
    }
}

현재는 paddingValuesfillMaxSize()를 사용하지 않아, 리스트가 나올 때와 레이아웃 느낌이 달라질 수 있습니다. 아래처럼 Box로 감싸서 중앙 정렬 + padding까지 적용하면 UX가 조금 더 자연스러워집니다.

is PostListUiState.Loading -> {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(paddingValues),
        contentAlignment = Alignment.Center
    ) {
        CircularProgressIndicator()
    }
}

is PostListUiState.Error -> {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(paddingValues),
        contentAlignment = Alignment.Center
    ) {
        Text("로딩 실패")
    }
}
  1. 초기 진입 시 네트워크 요청이 두 번 발생할 수 있음
  • PostListViewModelinit { loadPosts() }
  • PostListScreenLaunchedEffect(Unit) { viewModel.refresh() }

두 군데에서 모두 최초 로딩을 트리거하고 있어서, 첫 화면 진입 시 동일한 API 요청이 두 번 나갈 수 있습니다.
의도적으로 항상 강제 새로고침을 하고 싶지 않다면,

  • ViewModel 쪽 init 로딩을 제거하고 화면에서만 refresh()를 관리하거나,
  • 반대로 LaunchedEffect 없이 init만 사용하도록 정리하는 것을 고려해볼 수 있습니다.

Also applies to: 56-84

app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt (1)

12-91: Result + runCatching 패턴은 일관적이지만, 로그와 중복 로직을 조금 다듬을 수 있습니다

현재 구현은 전반적으로 다음 패턴이 잘 통일되어 있어서 읽기 좋습니다.

  • runCatching { apiService.XXX() }로 네트워크 호출 래핑
  • if (response.success && response.data != null) response.data else throw Exception(...)
  • .onFailure { Log.e("PostRepository", error.message.toString()) }

다만 두 가지 정도 개선 여지가 있어 보입니다.

  1. Log.e에 throwable을 함께 넘기기

지금은 메시지만 로그에 남아서 스택트레이스를 확인하기 어렵습니다. Android 로그 관점에서는 아래처럼 throwable을 함께 넘기는 것이 디버깅에 훨씬 유리합니다.

.onFailure { error ->
    Log.e("PostRepository", error.message ?: "PostRepository error", error)
}
  1. 성공/실패 분기 로직 중복 제거

getPosts, createPost, getPostDetail, updatePost, uploadImage가 모두 비슷한 구조를 가지고 있으므로, 예를 들어 다음과 같은 private 헬퍼로 공통화하면 유지보수에 도움이 됩니다.

private inline fun <T> handleResponse(
    call: () -> BaseResponse<T>,
    defaultErrorMessage: String
): Result<T> = runCatching {
    val response = call()
    if (response.success && response.data != null) {
        response.data
    } else {
        throw Exception(response.message ?: defaultErrorMessage)
    }
}.onFailure { error ->
    Log.e("PostRepository", error.message ?: defaultErrorMessage, error)
}

이후 각 메서드에서는 handleResponse({ apiService.getPosts() }, "게시글 불러오기 실패") 식으로 사용할 수 있습니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt (2)

19-20: 초기 상태를 Idle로 변경하는 것을 권장합니다.

게시글 작성 전에는 아무 작업도 진행 중이지 않으므로, Loading 상태보다는 Idle 또는 유사한 초기 상태가 더 적합합니다. Loading은 실제로 작업이 시작되었을 때만 설정되어야 합니다.

-private val _uiState = MutableStateFlow<PostCreateUiState>(PostCreateUiState.Loading)
+private val _uiState = MutableStateFlow<PostCreateUiState>(PostCreateUiState.Idle)

39-42: 에러 메시지를 더 구체적으로 개선할 수 있습니다.

현재 에러 메시지 폴백이 "error"로 설정되어 있어 사용자와 개발자 모두에게 유용한 정보를 제공하지 못합니다.

 .onFailure { error ->
     _uiState.value = PostCreateUiState.Error(
-        error.message ?: "error"
+        error.message ?: "게시글 작성에 실패했습니다"
     )
 }
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt (1)

6-6: 사용하지 않는 import를 제거해주세요.

retrofit2.http.Body import가 이 파일에서 사용되지 않습니다. Retrofit 어노테이션은 ApiService에서만 필요하며, Repository 인터페이스에서는 불필요합니다.

 import com.example.kuit6_android_api.data.model.response.PostResponse
 import okhttp3.MultipartBody
-import retrofit2.http.Body
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt (2)

72-85: 삭제 처리 로직이 잘 구현되었습니다.

deleteUiState 변화에 따른 처리가 적절하게 구현되어 있습니다. 다만 에러 발생 시 사용자에게 에러 메시지를 표시하는 Toast나 Snackbar를 추가하면 더 나은 UX를 제공할 수 있습니다.


107-217: Loading 및 Error 상태 UI를 개선해주세요.

현재 Loading 상태에서 아무것도 표시하지 않고, Error 상태에서는 "로딩 실패"라는 일반적인 메시지만 표시합니다. 사용자 경험을 위해 로딩 인디케이터와 구체적인 에러 메시지를 표시하는 것을 권장합니다.

 when (uiState) {
     is PostDetailUiState.Loading -> {
-
+        Box(
+            modifier = Modifier.fillMaxSize(),
+            contentAlignment = Alignment.Center
+        ) {
+            CircularProgressIndicator()
+        }
     }
     
     // ... Success case ...
     
     is PostDetailUiState.Error -> {
-        Text("로딩 실패")
+        Box(
+            modifier = Modifier.fillMaxSize().padding(16.dp),
+            contentAlignment = Alignment.Center
+        ) {
+            Text(
+                text = (uiState as PostDetailUiState.Error).message,
+                color = MaterialTheme.colorScheme.error
+            )
+        }
     }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5613545 and 1867fab.

📒 Files selected for processing (37)
  • .idea/deploymentTargetSelector.xml (1 hunks)
  • .idea/gradle.xml (1 hunks)
  • CLAUDE.md (0 hunks)
  • app/build.gradle.kts (4 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/api/ApiService.kt (1 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/model/Post.kt (0 hunks)
  • app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthResponse.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/data/model/response/ImageUploadResponse.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/data/model/response/PostResponse.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/component/PostItem.kt (2 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 (6 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/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/UploadImageUiState.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/ui/post/viewmodel/UriUtils.kt (1 hunks)
  • gradle/libs.versions.toml (1 hunks)
💤 Files with no reviewable changes (3)
  • app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt
  • CLAUDE.md
  • app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
🔇 Additional comments (26)
.idea/deploymentTargetSelector.xml (1)

7-7: IDE 메타데이터 파일로 검토 대상 외입니다.

이 파일은 Android Studio의 배포 타겟 선택 메타데이터 파일이며, 타임스탐프 업데이트는 IDE가 자동으로 수행하는 설정 변경입니다. 기능 코드 변경이 아니므로 코드 리뷰 대상이 아닙니다.

app/src/main/java/com/example/kuit6_android_api/data/model/response/ImageUploadResponse.kt (1)

6-9: ImageUploadResponse 파일은 현재 사용되지 않는 코드입니다

검증 결과, ImageUploadResponse 클래스가 어디서도 임포트되거나 사용되지 않음을 확인했습니다. 실제 API 응답은 ApiService에서 BaseResponse<Map<String, String>>으로 반환되며, PostRepositoryImpl도 직접 Map<String, String> 타입으로 처리하고 있습니다.

따라서 원본 리뷰에서 제기한 imageUrl의 nullability 문제는 현재 실제 동작 코드에 영향을 주지 않습니다. 이 파일은 정리되거나 실제 구현과 일치하도록 수정이 필요합니다.

Likely an incorrect or invalid review comment.

app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.kt (1)

1-15: PostDetail 화면용 UiState 설계가 명확합니다.

Loading / Success(PostResponse) / Error(message) 세 가지 상태로 잘 나뉘어 있어서 ViewModel 과 화면에서 상태 기반으로 분기하기 좋습니다. 추후 삭제 결과나 다른 부가 상태가 필요해지면 sealed class 에 case 를 확장하기도 편해 보입니다.

gradle/libs.versions.toml (1)

62-62: Retrofit용 Kotlinx Serialization 컨버터 항목이 잘 추가되었습니다.

retrofit 버전 ref 를 공유하도록 정의해서 Retrofit 본체와 컨버터 버전을 함께 관리하기 좋아 보입니다. 실제 Retrofit 설정 코드에서 이 라이브러리를 사용하고 있을 텐데, Gradle sync 시 문제 없고 사용 중인 retrofit2-kotlinx-serialization API 와도 호환되는지만 한 번 더 확인해 주세요.

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

1-15: PostList 화면용 UiState가 Detail 과 일관되게 잘 정의되어 있습니다.

목록 화면도 Loading / Success(posts) / Error(message) 로 명확하게 모델링되어 있어서 PostListViewModel 의 refresh() 결과를 상태 기반으로 처리하기 좋습니다. 추후 빈 목록을 별도 상태로 표현하고 싶어질 때도 sealed class 에 case 추가만으로 확장이 가능해 보입니다.

app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt (1)

1-12: 공통 API 응답 래퍼(BaseResponse) 도입이 좋아 보입니다.

success / message / data / timestamp 를 하나의 제네릭 모델로 묶어 두어서 ApiService 와 Repository 에서 응답을 일관되게 처리하고 에러 메시지도 공통으로 다루기 쉬울 것 같습니다. 다만 data 가 nullable 이므로, Repository 계층에서 success == true 인데 data == null 인 경우를 어떻게 처리할지(예외 throw, 별도 에러 변환 등)만 명확히 정해 두면 이후 ViewModel/UiState 쪽에서도 더 안전하게 사용할 수 있을 것 같습니다.

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

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

DI 컨테이너 초기화를 위한 App 클래스 등록이 적절합니다.

app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt (1)

6-11: 요청 DTO 구조가 올바르게 설계되었습니다.

직렬화 어노테이션이 적절하게 사용되었고, nullable 필드 처리도 정확합니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt (1)

5-15: UI 상태 관리 구조가 적절합니다.

sealed class를 사용한 상태 모델링이 올바르게 구현되었습니다.

app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt (1)

17-36: 네트워크 클라이언트 설정이 적절합니다.

OkHttp 타임아웃 설정과 Retrofit 구성이 올바르게 구현되었습니다. Kotlinx Serialization의 ignoreUnknownKeyscoerceInputValues 설정은 API 변경에 대한 유연성을 제공합니다.

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

10-20: App 클래스 매니페스트 등록 확인됨 - 코드 변경사항 승인

검증 결과, AndroidManifest.xml 파일의 line 17에 App 클래스가 올바르게 등록되어 있습니다. 따라서 PostViewModelFactory.kt의 lines 14-15에서 수행되는 App 타입 캐스팅은 안전하며, reified 타입 파라미터와 ViewModelFactory를 사용한 ViewModel 팩토리 구현이 타입 안전성을 충족합니다.

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

8-16: 잘 구현된 DI 컨테이너입니다.

수동 의존성 주입이 명확하고 간결하게 구현되었습니다. lazy 초기화를 사용하여 필요할 때만 인스턴스를 생성하는 것도 좋은 접근입니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt (1)

5-15: UI 상태 모델이 잘 정의되었습니다.

로딩, 성공, 에러 상태가 명확하게 구분되어 있으며, 타입 안전성이 보장됩니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/UriUtils.kt (2)

10-26: 리소스 관리가 안전하게 구현되었습니다.

use 블록을 사용하여 InputStream과 FileOutputStream이 자동으로 닫히도록 했으며, 예외 처리도 적절합니다.


28-40: Cursor 리소스 관리가 올바르게 구현되었습니다.

use 블록을 사용하여 Cursor가 자동으로 닫히도록 했으며, getColumnIndex() 결과를 체크하여 안전하게 처리했습니다.

app/src/main/java/com/example/kuit6_android_api/data/model/response/PostResponse.kt (1)

6-15: API 응답 모델이 명확하게 정의되었습니다.

kotlinx.serialization을 활용한 직렬화 설정이 적절하며, nullable 필드(imageUrl)도 올바르게 처리되었습니다.

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

25-40: 게시글 로딩 로직이 잘 구현되었습니다.

Repository 패턴을 통한 데이터 로딩과 에러 처리가 적절하게 구현되었습니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt (2)

27-45: 게시글 수정 로직이 잘 구현되었습니다.

Repository를 통한 데이터 업데이트와 상태 관리가 적절하게 구현되었습니다.


48-67: 이미지 업로드 및 초기화 로직이 잘 구현되었습니다.

이미지 업로드 상태 관리와 초기화 함수가 명확하게 분리되어 있습니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/component/PostItem.kt (1)

32-39: 응답 모델(PostResponse/AuthorResponse)로의 치환이 자연스럽게 잘 되어 있습니다

PostResponse / AuthorResponse로 타입을 교체하면서 사용하는 필드(author.username, author.profileImageUrl, createdAt 등)가 모두 일관되게 맞춰져 있고, Preview에서도 실제 응답 구조를 반영하고 있어서 이후 API 연동/디버깅에 도움이 될 것 같습니다. 추가 수정 필요 없어 보입니다.

Also applies to: 156-163

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

35-38: PostListUiState 기반 분기와 ClassCastException 회피 방식이 적절합니다

postListUiState.collectAsState()로 단일 UiState를 받고, when (uiState)에서 Success일 때만

val posts = (uiState as PostListUiState.Success).posts
LazyColumn { items(posts) { ... } }

처럼 posts를 안정된 로컬 값으로 꺼내 LazyColumn에 넘긴 구조가 좋습니다. 이전 Composition에서 캡처된 uiState로 인한 ClassCastException을 잘 피하면서도, UI 쪽에서 필요한 정보만 전달하도록 깔끔하게 정리되었습니다.

Also applies to: 56-81

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

48-69: 이미지 업로드 상태 관리가 잘 구현되었습니다.

이미지 업로드 상태를 게시글 작성 상태와 분리하여 관리하는 것은 좋은 설계입니다. clearUploadedImageUrl() 함수를 통해 상태를 명시적으로 초기화할 수 있어 UI 흐름 제어가 명확합니다.

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

37-37: ViewModel 팩토리 패턴이 잘 적용되었습니다.

각 화면에 특화된 ViewModel을 팩토리를 통해 일관되게 생성하고 있으며, Repository 의존성 주입이 명확하게 구현되어 있습니다. 수동 DI 패턴을 올바르게 활용한 좋은 구조입니다.

Also applies to: 52-56, 71-75, 93-97

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

8-28: Repository 인터페이스 설계가 훌륭합니다.

Result 타입을 사용하여 타입 안전한 에러 처리를 제공하고, API 계층으로부터 깨끗한 추상화를 제공합니다. suspend 함수 사용도 적절하며, 인터페이스가 명확하고 일관성 있게 설계되었습니다.

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

61-69: 상태 구독 및 데이터 로딩이 올바르게 구현되었습니다.

collectAsState를 사용한 상태 구독과 LaunchedEffect(Unit)을 통한 데이터 로딩이 적절하게 구현되었습니다. 화면 진입 시 자동으로 상세 정보를 불러오는 흐름이 명확합니다.

app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt (1)

1-54: 라이브러리 버전이 "latest"로 표시되어 있다는 것은 잘못된 가정입니다.

실제 빌드 구성(gradle/libs.versions.toml)을 확인한 결과, 라이브러리 버전이 명시적으로 고정되어 있습니다:

  • Retrofit: 2.11.0
  • OkHttp: 4.12.0

현재 최신 안정 버전은 Retrofit 3.0.0과 OkHttp 5.3.0이지만, 프로젝트에서 사용 중인 버전은 이전 메이저 버전으로 안정적이고 명시적으로 관리되고 있습니다. "latest" 문법으로 인한 버전 불안정성 문제는 없습니다.

Likely an incorrect or invalid review comment.

Comment thread app/build.gradle.kts
Comment on lines +23 to +29
val properties = Properties()
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
properties.load(localPropertiesFile.inputStream())
}
val baseUrl = properties.getProperty("BASE_URL")
buildConfigField("String", "BASE_URL", "\"$baseUrl\"")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

BASE_URL 설정 누락 시 조기 실패 및 스트림 정리를 해두는 것이 안전합니다.

현재 구현에서는 local.properties 가 없거나 BASE_URL 이 정의되지 않은 경우에도 buildConfigField("String", "BASE_URL", "\"$baseUrl\"") 가 실행되어 BASE_URL 값이 "null" 로 들어갈 수 있습니다. 빌드는 통과하지만 런타임에서 API 호출이 애매하게 실패할 수 있어서 원인 파악이 어려워질 수 있어요. 또한 inputStream()use 로 감싸서 스트림을 명시적으로 닫아주는 편이 좋습니다.

예를 들어 다음처럼 조기 실패 + 자원 정리를 해두면 더 안전합니다:

val properties = Properties()
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
    localPropertiesFile.inputStream().use { properties.load(it) }
}
val baseUrl = properties.getProperty("BASE_URL")
    ?: error("BASE_URL is not defined in local.properties")
buildConfigField("String", "BASE_URL", "\"$baseUrl\"")

이렇게 하면 설정이 잘못된 환경에서는 빌드 단계에서 바로 문제를 발견할 수 있습니다.

🤖 Prompt for AI Agents
In app/build.gradle.kts around lines 23–29, the current logic may set BASE_URL
to the string "null" when local.properties is missing or BASE_URL is not defined
and also leaves the InputStream unclosed; change it to fail fast and close the
stream: only load properties when the file exists using
localPropertiesFile.inputStream().use { ... } to ensure the stream is closed,
then read BASE_URL and if it's null throw an error (or call error(...)) so the
build fails early instead of writing a "null" value into buildConfigField;
finally pass the validated baseUrl into buildConfigField.

Comment on lines +24 to +26
suspend fun createPost(
@Query("author") author: String = "규빈",
@Body request: PostCreateRequest

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

API 서비스 계층에서 기본값을 제거해야 합니다.

author 파라미터에 하드코딩된 기본값 "규빈"이 설정되어 있습니다. 이는 비즈니스 로직이 API 정의에 결합되는 문제를 야기합니다. 기본값 처리는 ViewModel이나 Repository 계층에서 수행되어야 합니다.

 suspend fun createPost(
-    @Query("author") author: String = "규빈",
+    @Query("author") author: String,
     @Body request: PostCreateRequest
 ): BaseResponse<PostResponse>
📝 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
suspend fun createPost(
@Query("author") author: String = "규빈",
@Body request: PostCreateRequest
suspend fun createPost(
@Query("author") author: String,
@Body request: PostCreateRequest
): BaseResponse<PostResponse>
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt around
lines 24 to 26, the createPost API currently hardcodes a default value for the
author parameter ("규빈"); remove the hardcoded default so the function signature
requires an explicit author argument (i.e., change from author: String = "규빈" to
author: String), update the Retrofit annotation usage if needed to match the API
contract, and then update all callers (ViewModel/Repository) to supply the
author from business logic rather than relying on the API layer default.

Comment on lines +13 to +15
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

프로덕션 환경에서 민감한 데이터 로깅 위험

HttpLoggingInterceptor.Level.BODY는 요청/응답의 전체 본문을 로깅합니다. 프로덕션 빌드에서 민감한 정보(PII, 인증 토큰 등)가 로그에 노출될 수 있습니다.

디버그 빌드에서만 상세 로깅을 활성화하도록 수정하세요:

     private val loggingInterceptor = HttpLoggingInterceptor().apply {
-        level = HttpLoggingInterceptor.Level.BODY
+        level = if (BuildConfig.DEBUG) {
+            HttpLoggingInterceptor.Level.BODY
+        } else {
+            HttpLoggingInterceptor.Level.NONE
+        }
     }
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt
around lines 13 to 15, the logging interceptor is set to
HttpLoggingInterceptor.Level.BODY which can expose sensitive data in production;
change the configuration to enable BODY only for debug builds (use
BuildConfig.DEBUG) and set a safer level (NONE or BASIC) for non-debug builds,
and optionally add header redaction (e.g., Authorization) or a custom
interceptor to avoid logging PII/auth tokens.

Comment on lines +7 to +11
data class AuthorResponse(
@SerialName("id") val id: Long,
@SerialName("username") val username: String,
@SerialName("profileImageUrl") val profileImageUrl: String?
) No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

파일명과 클래스명 불일치

파일명은 AuthResponse.kt이지만 클래스명은 AuthorResponse입니다. 일관성을 위해 파일명을 AuthorResponse.kt로 변경하는 것을 권장합니다.

🤖 Prompt for AI Agents
app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthResponse.kt
lines 7-11: the Kotlin file name (AuthResponse.kt) does not match the data class
name (AuthorResponse); rename the file to AuthorResponse.kt to match the class
name, update any imports/usages that reference AuthResponse to the new
file/class location if necessary, and ensure package declarations remain
unchanged.

Comment on lines +66 to +68
navController.previousBackStackEntry
?.savedStateHandle
?.set("refreshNeeded", true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

refreshNeeded 플래그 소비를 확인해주세요.

게시글 생성 및 수정 후 savedStateHandlerefreshNeeded 플래그를 설정하고 있습니다. PostListScreen에서 이 플래그를 읽고 새로고침을 수행하는지 확인이 필요합니다.

Also applies to: 88-90


🏁 Script executed:

#!/bin/bash
# Description: PostListScreen에서 refreshNeeded 플래그 사용 확인

# PostListScreen.kt에서 savedStateHandle 또는 refreshNeeded 사용 검색
rg -nP -C5 'refreshNeeded|savedStateHandle' --type=kotlin -g '*PostListScreen.kt'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find PostListScreen file location
fd -t f 'PostListScreen\.kt' --type f

Length of output: 80


🏁 Script executed:

#!/bin/bash
# Search for savedStateHandle usage in the entire codebase to understand the pattern
rg -nP 'savedStateHandle' --type=kotlin -C3

Length of output: 1628


🏁 Script executed:

#!/bin/bash
# Check if refreshNeeded is referenced anywhere in the codebase
rg -nP 'refreshNeeded' --type=kotlin -C3

Length of output: 1732


🏁 Script executed:

#!/bin/bash
# Examine PostListScreen.kt to check if it accesses savedStateHandle or has refresh logic
cat app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt

Length of output: 3203


refreshNeeded 플래그가 PostListScreen에서 소비되지 않습니다.

검증 결과, NavGraph.kt의 66-68줄과 88-90줄에서 refreshNeeded 플래그를 설정하지만, PostListScreen.kt에서는 savedStateHandle을 전혀 접근하지 않습니다. 현재 PostListScreen은 초기 Composition 시점(LaunchedEffect(Unit))에만 새로고침이 발생하며, 게시글 생성/수정 후 돌아올 때는 새로고침이 동작하지 않습니다.

PostListScreen에서 savedStateHandlerefreshNeeded 플래그를 읽고 이를 기반으로 viewModel.refresh()를 호출하는 로직을 추가해야 합니다.

Comment on lines 295 to 306
Button(
// 작성하기 버튼 클릭 시 viewModel에서 createPost() 호출
onClick = {
val finalAuthor = author.ifBlank { "anonymous" }
viewModel.createPost(finalAuthor, title, content, null) {
onPostCreated()
}
val finalAuthor = author
val request = PostCreateRequest(
title = title,
content = content,
imageUrl = uploadedImageUrl
)
viewModel.createPost(finalAuthor, request)
onPostCreated()
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

createPost 결과와 상관없이 곧바로 onPostCreated()를 호출하는 동작은 위험합니다

현재 작성 버튼 클릭 시 로직은 다음과 같습니다.

val request = PostCreateRequest(
    title = title,
    content = content,
    imageUrl = uploadedImageUrl
)
viewModel.createPost(finalAuthor, request)
onPostCreated()

createPost가 내부에서 코루틴으로 비동기 처리될 가능성이 높은데,
API 호출 성공 여부와 관계없이 바로 onPostCreated()를 호출하면서 화면을 닫는 구조입니다.

이렇게 되면:

  • 네트워크 오류나 서버 에러로 실제 글 작성이 실패해도 리스트 화면으로 돌아가고,
  • 사용자는 작성 실패를 인지하지 못한 채로 “작성 성공”으로 오해할 수 있습니다.

이미 uiState by viewModel.uiState.collectAsState()를 구독하고 계시므로,

  1. ViewModel에서 작성 성공/실패를 PostCreateUiState(예: Success, Error)로 노출하고,
  2. 화면에서는 LaunchedEffect(uiState)를 사용해 성공일 때만 onPostCreated()를 호출,
  3. 실패 시에는 Toast나 Dialog로 에러 메시지를 보여주는 패턴

으로 옮기는 것을 권장드립니다.

또한 작성 요청이 진행 중일 때는 버튼을 비활성화하는 조건에 uiState is Loading 같은 플래그를 추가하면 중복 요청도 방지할 수 있습니다.

Also applies to: 310-311


I'll verify the review comment by examining the ViewModel implementation, UiState structure, and current code patterns.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find the PostCreateViewModel file
fd -e kt -e java | xargs grep -l "class PostCreateViewModel" | head -20

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Search for PostCreateUiState definition
fd -e kt -e java | xargs grep -l "PostCreateUiState\|class.*UiState" | grep -i post | head -20

Length of output: 583


🏁 Script executed:

#!/bin/bash
# Look at the full PostCreateScreen.kt file around the target lines
fd "PostCreateScreen.kt"

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Search for createPost method implementation
fd -e kt -e java | xargs grep -n "fun createPost" | head -20

Length of output: 452


🏁 Script executed:

#!/bin/bash
# Check for LaunchedEffect usage patterns in Kotlin compose files
fd -e kt | xargs grep -l "LaunchedEffect" | head -10

Length of output: 327


🏁 Script executed:

#!/bin/bash
# Look for collectAsState usage in PostCreateScreen or similar files
fd -e kt | xargs grep -l "collectAsState\|uiState" | grep -i post | head -20

Length of output: 679


🏁 Script executed:

#!/bin/bash
# Search for error handling patterns with UI state
fd -e kt | xargs grep -B3 -A3 "is.*Error\|is.*Success\|when.*uiState" | head -50

Length of output: 5355


🏁 Script executed:

#!/bin/bash
# Find all Kotlin files related to post creation
fd -path "*post*" -e kt | head -30

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Look for Loading state usage
fd -e kt | xargs grep -n "is Loading\|Loading()" | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for suspend functions in ViewModel files
fd -e kt | xargs grep -B2 "suspend fun" | grep -A2 -B2 createPost

Length of output: 1247



---

🏁 Script executed:

```shell
#!/bin/bash
# View PostCreateViewModel.kt to see createPost implementation
cat app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt

Length of output: 2580


🏁 Script executed:

#!/bin/bash
# View PostCreateUiState.kt to see state structure
cat app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt

Length of output: 367


🏁 Script executed:

#!/bin/bash
# View PostCreateScreen.kt full file
cat app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt

Length of output: 13775


🏁 Script executed:

#!/bin/bash
# Check PostEditScreen.kt for comparison (similar use case)
cat app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt | head -400

Length of output: 12221


🏁 Script executed:

#!/bin/bash
# Check how PostDetailScreen uses LaunchedEffect with DeletePostUiState
grep -A 20 "LaunchedEffect(deleteUiState)" app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt

Length of output: 627


PostCreateScreen의 비동기 작업 결과 미확인 문제 확인됨

검증 결과, 리뷰 의견이 정확합니다. 현재 코드는 다음과 같은 문제가 있습니다:

문제점:

  • ViewModel의 createPost()는 올바르게 LoadingSuccess/Error 상태 변화를 방출합니다
  • 그러나 UI에서 uiState를 구독(collectAsState)하고도 이를 활용하지 않습니다
  • onPostCreated() 호출이 비동기 작업 완료를 기다리지 않고 즉시 실행됩니다
  • 버튼은 이미지 업로드 중(imgUiState.Loading)만 비활성화되며, 게시글 작성 중(uiState.Loading)은 무시됩니다

코드베이스 내 모순:
PostDetailScreen에서는 올바르게 구현되어 있습니다:

LaunchedEffect(deleteUiState) {
    when (deleteUiState) {
        is DeletePostUiState.Success -> onNavigateBack()
        // ...
    }
}

PostCreateScreen도 동일한 패턴으로 수정 필요:

  1. LaunchedEffect(uiState)로 상태 변화 감시
  2. Success일 때만 onPostCreated() 호출
  3. Error일 때 에러 메시지 표시 (이미지 업로드 패턴 참고)
  4. Loading 중 버튼 비활성화 추가
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt
around lines 295-306, the button handler currently calls
viewModel.createPost(...) and immediately invokes onPostCreated() without
waiting for the ViewModel's createPost UI state; update the screen to observe
the ViewModel uiState (use collectAsState and a LaunchedEffect(uiState)), remove
the immediate onPostCreated() call from the onClick, call onPostCreated() only
when uiState is Success, show an error message when uiState is Error (following
the image-upload error pattern), and disable the submit Button whenever uiState
is Loading (and still while imgUiState is Loading).

Comment on lines +113 to 211
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(paddingValues)
.background(MaterialTheme.colorScheme.background)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
// 작성자 정보 (상단)
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.surface,
tonalElevation = 1.dp
) {
// 프로필 이미지
if (it.author.profileImageUrl != null) {
AsyncImage(
model = it.author.profileImageUrl,
contentDescription = "프로필 이미지",
modifier = Modifier
.size(44.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
} else {
Surface(
modifier = Modifier.size(44.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = "기본 프로필",
modifier = Modifier.padding(10.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// 프로필 이미지
if ((uiState as PostDetailUiState.Success).post.author.profileImageUrl != null) {
AsyncImage(
model = (uiState as PostDetailUiState.Success).post.author.profileImageUrl,
contentDescription = "프로필 이미지",
modifier = Modifier
.size(44.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
} else {
Surface(
modifier = Modifier.size(44.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = "기본 프로필",
modifier = Modifier.padding(10.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}

Spacer(modifier = Modifier.width(12.dp))

Column {
Text(
text = it.author.username,
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.SemiBold
),
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = formatDateTime(it.createdAt),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(12.dp))

Column {
Text(
text = (uiState as PostDetailUiState.Success).post.author.username,
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.SemiBold
),
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = formatDateTime((uiState as PostDetailUiState.Success).post.createdAt),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}

// 이미지
it.imageUrl?.let { imageUrl ->
AsyncImage(
model = imageUrl,
contentDescription = "게시글 이미지",
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 400.dp),
contentScale = ContentScale.Fit
)
}
// 이미지
(uiState as PostDetailUiState.Success).post.imageUrl?.let { imageUrl ->
AsyncImage(
model = imageUrl,
contentDescription = "게시글 이미지",
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 400.dp),
contentScale = ContentScale.Fit
)
}

// 제목과 내용
Column(
modifier = Modifier.padding(20.dp)
) {
Text(
text = it.title,
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.Bold
),
color = MaterialTheme.colorScheme.onBackground
)

Spacer(modifier = Modifier.height(16.dp))

Text(
text = it.content,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
lineHeight = MaterialTheme.typography.bodyLarge.lineHeight.times(1.5f)
)

Spacer(modifier = Modifier.height(40.dp))
// 제목과 내용
Column(
modifier = Modifier.padding(20.dp)
) {
Text(
text = (uiState as PostDetailUiState.Success).post.title,
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.Bold
),
color = MaterialTheme.colorScheme.onBackground
)

Spacer(modifier = Modifier.height(16.dp))

Text(
text = (uiState as PostDetailUiState.Success).post.content,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
lineHeight = MaterialTheme.typography.bodyLarge.lineHeight.times(1.5f)
)

Spacer(modifier = Modifier.height(40.dp))
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

반복된 타입 캐스팅을 제거해야 합니다.

Success 브랜치 내에서 (uiState as PostDetailUiState.Success).post를 여러 번 반복하여 캐스팅하고 있습니다. 이는 비효율적이며 가독성을 해치고 오류 가능성을 높입니다. 한 번만 캐스팅하여 변수에 저장한 후 재사용해야 합니다.

 is PostDetailUiState.Success -> {
+    val post = (uiState as PostDetailUiState.Success).post
     Column(
         modifier = Modifier
             .fillMaxSize()
             // ...
     ) {
         Surface(
             // ...
         ) {
             Row(
                 // ...
             ) {
                 // 프로필 이미지
-                if ((uiState as PostDetailUiState.Success).post.author.profileImageUrl != null) {
+                if (post.author.profileImageUrl != null) {
                     AsyncImage(
-                        model = (uiState as PostDetailUiState.Success).post.author.profileImageUrl,
+                        model = post.author.profileImageUrl,
                         // ...
                     )
                 } else {
                     // ...
                 }
                 
                 Spacer(modifier = Modifier.width(12.dp))
                 
                 Column {
                     Text(
-                        text = (uiState as PostDetailUiState.Success).post.author.username,
+                        text = post.author.username,
                         // ...
                     )
                     Text(
-                        text = formatDateTime((uiState as PostDetailUiState.Success).post.createdAt),
+                        text = formatDateTime(post.createdAt),
                         // ...
                     )
                 }
             }
         }
         
         // 이미지
-        (uiState as PostDetailUiState.Success).post.imageUrl?.let { imageUrl ->
+        post.imageUrl?.let { imageUrl ->
             AsyncImage(
                 model = imageUrl,
                 // ...
             )
         }
         
         // 제목과 내용
         Column(
             modifier = Modifier.padding(20.dp)
         ) {
             Text(
-                text = (uiState as PostDetailUiState.Success).post.title,
+                text = post.title,
                 // ...
             )
             
             Spacer(modifier = Modifier.height(16.dp))
             
             Text(
-                text = (uiState as PostDetailUiState.Success).post.content,
+                text = post.content,
                 // ...
             )
             
             Spacer(modifier = Modifier.height(40.dp))
         }
     }
 }

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

🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt
around lines 113 to 211, you're repeatedly casting uiState to
PostDetailUiState.Success for every access; create a single local val (e.g., val
post = (uiState as PostDetailUiState.Success).post) at the start of the Success
branch and then replace all subsequent occurrences of (uiState as
PostDetailUiState.Success).post with that local variable to avoid redundant
casts and improve readability.

Comment on lines +249 to +260
Button(
onClick = {
val finalImageUrl = uploadedImageUrl
val request = PostCreateRequest(
title = title,
content = content,
imageUrl = finalImageUrl
)
// 수정 버튼 누를 시 PostEditViewModel의 editPost() 호출
viewModel.editPost(postId, request)
onPostUpdated()
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

게시글 수정 완료 전에 콜백이 호출됩니다.

onPostUpdated()editPost() 호출 직후 즉시 실행되어, 실제 수정이 완료되기 전에 화면 전환이 발생할 수 있습니다. 이는 사용자가 업데이트되지 않은 데이터를 볼 수 있는 문제를 야기합니다.

수정 성공 여부를 확인한 후 콜백을 호출하도록 수정하세요:

+// 수정 성공 시 콜백 호출을 위한 LaunchedEffect 추가
+LaunchedEffect(uiState) {
+    if (uiState is PostEditUiState.Success) {
+        onPostUpdated()
+    }
+}
+
 Button(
     onClick = {
         val finalImageUrl = uploadedImageUrl
         val request = PostCreateRequest(
             title = title,
             content = content,
             imageUrl = finalImageUrl
         )
-        // 수정 버튼 누를 시 PostEditViewModel의 editPost() 호출
         viewModel.editPost(postId, request)
-        onPostUpdated()
     },
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt
around lines 249-260, the onPostUpdated() callback is invoked immediately after
calling viewModel.editPost(), causing navigation before the edit completes;
change the flow so the callback is invoked only after a successful edit: have
editPost return a result (or expose a success LiveData/StateFlow in the
ViewModel), call editPost from a coroutine or trigger it and observe the
ViewModel's success state, and invoke onPostUpdated() only when that success
state is true (handle errors by showing a message instead of navigating).

Comment on lines +6 to +8
data class Success(
val deletePost : Unit
) : DeletePostUiState()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Success 상태를 data object로 변경하세요.

Unit 타입의 속성을 가진 data class는 의미가 없습니다. 삭제 성공 시 반환할 데이터가 없다면 data object를 사용하는 것이 더 적절합니다.

-data class Success(
-    val deletePost : Unit
-) : DeletePostUiState()
+data object Success : DeletePostUiState()
📝 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
data class Success(
val deletePost : Unit
) : DeletePostUiState()
data object Success : DeletePostUiState()
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/state/DeletePostUiState.kt
around lines 6 to 8, replace the meaningless data class Success(val deletePost:
Unit) with a singleton data object (data object Success : DeletePostUiState())
so the success state carries no redundant Unit property; update any code that
constructed Success(...) to just reference Success.

private val _uploadImageUiState = MutableStateFlow<UploadImageUiState>(UploadImageUiState.Idle)
val uploadImageUiState: StateFlow<UploadImageUiState> = _uploadImageUiState.asStateFlow()

// 게시글 수정 시 호출하는 함수 -> 성공 시 반환되는 response를 Sucess(pot)로 uiState에 반환

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

주석의 오타를 수정하세요.

"Sucess" → "Success"

-// 게시글 수정 시 호출하는 함수 -> 성공 시 반환되는 response를 Sucess(pot)로 uiState에 반환
+// 게시글 수정 시 호출하는 함수 -> 성공 시 반환되는 response를 Success(post)로 uiState에 반환
📝 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
// 게시글 수정 시 호출하는 함수 -> 성공 시 반환되는 response를 Sucess(pot)로 uiState에 반환
// 게시글 수정 시 호출하는 함수 -> 성공 시 반환되는 response를 Success(post)로 uiState에 반환
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt
around line 26, fix the typo in the comment by changing "Sucess" to "Success" so
the comment reads correctly; update the Korean comment if needed to keep meaning
intact (e.g., "성공 시 반환되는 response를 Success(post)로 uiState에 반환").

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.

3 participants