Skip to content

8주차 미션 / 안드로이드 2조 사예원#28

Open
BearYeWon wants to merge 16 commits into
Konkuk-KUIT:BearYeWonfrom
BearYeWon:BearYeWon/week8
Open

8주차 미션 / 안드로이드 2조 사예원#28
BearYeWon wants to merge 16 commits into
Konkuk-KUIT:BearYeWonfrom
BearYeWon:BearYeWon/week8

Conversation

@BearYeWon

@BearYeWon BearYeWon commented Nov 19, 2025

Copy link
Copy Markdown

📝 미션

미션: 자동 로그인 구현 및 토큰 검증

🙋 구현에 대한 설명

  • 실습 내용 구현해오기
  • AuthInterceptor 구현
    • DataStore에서 토큰 불러오기
    • 헤더에 토큰 담아서 검증 요청 보내기 (Interceptor를 통해 하셔야 합니다)
  • LoginScreen 검증 추가
    • 검증 버튼 추가
    • 검증 버튼 클릭 시 버튼 텍스트 바꾸기
    • 자동 로그인 구현하기
      • 자동 로그인 Boolean 값 DataStore에 저장하기
      • 자동 로그인 Boolean 값 불러와서 버튼 클릭하지 않아도 검증 요청 보내게 하기

미션 구현 설명

[Interceptor]

  • 인터셉터 통해서 헤더에 authorize 정보 추가

[TokenRepositoryImpl]

  • 토큰 검증을 위한 getValidateTokenApi, validateToken, deleteToken 함수 추가

[LoginUiState]

  • 토큰 검증을 위한 상태 tokenValidationState, isLoading 추가
  • TokenValidationState class 추가해 토큰 검증 상태 정의

[LoginViewModel]

  • initAutoLogin : 자동 로그인 값 DataStore에서 가져와 값에 따라 자동 토큰 검증 시도
  • validateToken : 토큰 검증 함수를 tokenRepository에서 가져와 validateToken() 호출 후 성공 시 state를 Success, 실패 시 Failure

[LoginScreen]

  • LaunchedEffect 사용해 스크린 진입 시마다 initAutoLogin() 호출해 로그인 상태 확인 및 자동 로그인 시 validateToken 자동 호출하도록 구현

📷 스크린샷 & 실행영상

8.mp4

🎸 기타

Summary by CodeRabbit

Release Notes

  • 새로운 기능
    • 사용자 인증 시스템 추가 (로그인, 회원가입)
    • 게시물 작성 시 이미지 업로드 지원
    • 자동 로그인 및 토큰 기반 인증 기능
    • 게시물 생성, 수정, 삭제 작업 개선

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Nov 19, 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.

검토 요약

Walkthrough

API 레이어를 구성하고, 저장소 패턴을 구현하며, 로그인 및 이미지 업로드 기능을 추가하는 데이터-뷰모델 아키텍처 리팩토링입니다. 기존 더미 모델을 실제 네트워크 기반 모델로 교체하고, UI 상태 관리를 도입합니다.

Changes

코호트 / 파일(s) 변경 사항 요약
데이터 계층 - API 및 네트워킹
app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt, RetrofitClient.kt, AuthInterceptor.kt
Retrofit을 통한 API 엔드포인트 정의, OkHttpClient 구성, Bearer 토큰 기반 인증 인터셉터 추가
데이터 계층 - 의존성 주입
app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt
저장소 및 API 서비스의 lazy 초기화를 제공하는 경량 DI 컨테이너
데이터 계층 - 응답 모델
app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt, PostResponse.kt, AuthorResponse.kt, LoginResponse.kt, ImageUploadResponse.kt
직렬화 가능한 데이터 클래스로 API 응답 구조 정의
데이터 계층 - 요청 모델
app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt, LoginRequest.kt
직렬화 가능한 데이터 클래스로 API 요청 구조 정의
데이터 계층 - 저장소 인터페이스 및 구현
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt, PostRepositoryImpl.kt, LoginRepository.kt, LoginRepositoryImpl.kt, TokenRepository.kt, TokenRepositoryImpl.kt
CRUD 작업 및 인증/토큰 관리를 위한 저장소 패턴 구현 (Result 래핑 포함)
UI 계층 - 상태 관리
app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt, PostDetailUiState.kt, PostCreateUiState.kt, PostEditUiState.kt, LoginUiState.kt, TokenValidationState.kt, DeletePostUiState.kt, UploadImageUiState.kt
로딩·성공·오류 상태를 나타내는 sealed 클래스 계층 구조
UI 계층 - 뷰모델
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt, PostDetailViewModel.kt, PostCreateViewModel.kt, PostEditViewModel.kt, LoginViewModel.kt
StateFlow를 통한 상태 기반 UI 로직 (기존 단일 PostViewModel 대체)
UI 계층 - 뷰모델 팩토리
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt, LoginViewModelFactory.kt
저장소 주입을 지원하는 ViewModel 생성 팩토리
UI 계층 - 유틸리티
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/LoginScreen.kt, PostListScreen.kt, PostDetailScreen.kt, PostCreateScreen.kt, PostEditScreen.kt, component/PostItem.kt, navigation/NavGraph.kt, navigation/Routes.kt
새로운 로그인 화면 추가, 상태 기반 렌더링으로 기존 화면 리팩토링, 네비게이션 확장
애플리케이션 진입점 및 설정
app/src/main/java/com/example/kuit6_android_api/App.kt, app/src/main/AndroidManifest.xml, app/build.gradle.kts, .coderabbit.yaml
싱글톤 애플리케이션 클래스, Retrofit/Datastore 의존성 추가, BASE_URL 환경설정, CodeRabbit 자동 검토 설정
모델 삭제 및 IDE 설정
app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt (삭제), CLAUDE.md (삭제), .idea/gradle.xml, .idea/deploymentTargetSelector.xml
레거시 더미 모델 제거, 프로젝트 설정 업데이트
Gradle 버전 카탈로그
gradle/libs.versions.toml
DataStore Preferences 및 Retrofit Kotlinx Serialization 변환기 의존성 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant LoginScreen
    participant LoginViewModel
    participant LoginRepository
    participant ApiService
    participant TokenRepository
    participant TokenStorage

    User->>LoginScreen: 로그인 입력 및 제출
    LoginScreen->>LoginViewModel: login(context)
    LoginViewModel->>LoginRepository: login(id, password)
    LoginRepository->>ApiService: login(LoginRequest)
    ApiService-->>LoginRepository: BaseResponse<LoginResponse>
    alt 성공
        LoginRepository-->>LoginViewModel: Result.Success(LoginResponse)
        LoginViewModel->>TokenRepository: saveToken(context, token)
        TokenRepository->>TokenStorage: DataStore 저장
        TokenStorage-->>TokenRepository: 완료
        LoginViewModel->>LoginScreen: uiState 업데이트 (Success)
        LoginScreen->>User: 로그인 완료
    else 실패
        LoginRepository-->>LoginViewModel: Result.Failure(Exception)
        LoginViewModel->>LoginScreen: uiState 업데이트 (Error)
        LoginScreen->>User: 오류 메시지 표시
    end
Loading
sequenceDiagram
    participant User
    participant PostListScreen
    participant PostListViewModel
    participant PostRepository
    participant ApiService
    participant AuthInterceptor
    participant TokenRepository

    User->>PostListScreen: 화면 로드
    PostListScreen->>PostListViewModel: refresh()
    PostListViewModel->>PostRepository: getPosts()
    PostRepository->>ApiService: getPosts() (HTTP 요청)
    ApiService->>AuthInterceptor: 요청 가로채기
    AuthInterceptor->>TokenRepository: getToken(context)
    TokenRepository-->>AuthInterceptor: token
    AuthInterceptor->>ApiService: Authorization 헤더 추가 후 요청 진행
    ApiService-->>PostRepository: BaseResponse<List<PostResponse>>
    alt 성공
        PostRepository-->>PostListViewModel: Result.Success(posts)
        PostListViewModel->>PostListScreen: uiState = Success(posts)
        PostListScreen->>User: 게시물 목록 렌더링
    else 실패
        PostRepository-->>PostListViewModel: Result.Failure(Exception)
        PostListViewModel->>PostListScreen: uiState = Error(message)
        PostListScreen->>User: 오류 메시지 표시
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 분

주의가 필요한 영역:

  • 로그인/인증 흐름 검증: 토큰 저장소(DataStore), AuthInterceptor, TokenRepository 상호작용에서 토큰 관리 및 유효성 검증 로직의 정확성 확인
  • 이미지 업로드 구현: URI-to-File 변환, MultipartBody.Part 생성, 상태 관리(UploadImageUiState) 전반에서 메모리 누수 및 예외 처리 검토
  • 상태 관리 일관성: 5개의 신규 ViewModel과 8개의 상태 클래스 전반에 걸쳐 로딩·성공·오류 전이 및 데이터 일관성 확인
  • 저장소 패턴 예외 처리: 6개의 저장소 구현에서 Result 래핑, 로깅, 예외 메시지 처리의 일관성 검토
  • 네비게이션 변경사항: LoginRoute 추가, 기존 경로 수정, 상태 전파(onPostCreated/onPostUpdated) 메커니즘의 안정성 확인
  • UI 컴포넌트 마이그레이션: Post → PostResponse 타입 변경 및 상태 기반 렌더링 로직에서 null 안전성 및 분기 처리 확인

Possibly related PRs

  • Konkuk-KUIT/KUIT6_Android_API#18: ApiService, RetrofitClient, 요청/응답 모델, Post 모델 제거 및 Gradle 빌드 설정에 대한 중복 코드 수준의 변경사항
  • Konkuk-KUIT/KUIT6_Android_API#19: ApiService/RetrofitClient, 응답/요청 모델, Post 모델 교체, UI 화면/컴포넌트 및 .idea/gradle 모듈 경로 설정에 대한 겹치는 변경사항
  • Konkuk-KUIT/KUIT6_Android_API#17: 빌드 설정, ApiService/RetrofitClient, PostResponse/PostCreateRequest 모델, UriUtils, 뷰모델/화면 및 IDE 설정에 대한 중복 코드 수준의 변경사항

Poem

🐰 네트워크 연결로 생생한 데이터 흐르고,
상태 관리가 UI를 춤 추게 하네!
토큰으로 사용자를 보호하고,
사진 업로드도 거뜬하게 —
레이어 분리로 깔끔한 아키텍처 완성! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.34% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 변경 사항의 주요 내용과 관련이 있으나, 실제 기술적 구현 내용을 명확하게 반영하지 못하고 있습니다. '8주차 미션 / 안드로이드 2조 사예원'은 과제 번호와 팀 정보만 포함하며, 자동 로그인 및 토큰 검증 구현이라는 실제 변경 사항을 구체적으로 설명하지 않습니다.

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@alswlekk

Copy link
Copy Markdown
Contributor

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

✨ Finishing touches
🧪 Generate unit tests (beta)

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

Tip

📝 Customizable high-level summaries are now available in beta!
You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 20, 2025

Copy link
Copy Markdown

@alswlekk, I'll review the changes in this PR now.

✅ 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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.idea/deploymentTargetSelector.xml (1)

1-17: IDE 설정 파일을 저장소에서 제외하세요.

.idea/deploymentTargetSelector.xml은 Android Studio에서 자동 생성되는 설정 파일이며, 타임스탐프 변경만 포함되어 있습니다. 이 파일을 버전 제어에 포함하면 다음과 같은 문제가 발생합니다:

  • 개발자마다 다른 타임스탐프로 인한 불필요한 변경 로그 및 병합 충돌
  • 저장소 노이즈 증가
  • 실제 소스 코드 변경과 무관한 IDE 상태 추적

이 파일과 .idea/ 디렉토리 내 다른 IDE 설정 파일들을 .gitignore에 추가하고, 이미 커밋된 경우 다음과 같이 제거하세요:

git rm --cached .idea/deploymentTargetSelector.xml
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt (1)

295-328: viewModel.createPost() 호출 직후 바로 onPostCreated()를 호출하므로 생성 성공 여부와 무관하게 화면이 전환됩니다.

createPost()viewModelScope.launch에서 비동기로 실행되어 API 응답을 기다리지 않고, 즉시 onPostCreated()가 실행됩니다. 네트워크 요청이 진행 중이거나 실패해도 사용자가 목록 화면으로 이동하게 됩니다.

uiState가 이미 구독 중이므로, 다음과 같이 수정하여 Success 상태에서만 화면 전환을 처리하세요:

var hasCreateRequested by remember { mutableStateOf(false) }

Button(
    onClick = {
        hasCreateRequested = true
        viewModel.createPost(finalAuthor, request)
    },
    ...
)

LaunchedEffect(uiState, hasCreateRequested) {
    if (hasCreateRequested && uiState is PostCreateUiState.Success) {
        hasCreateRequested = false
        onPostCreated()
    }
}
🧹 Nitpick comments (20)
app/src/main/java/com/example/kuit6_android_api/data/model/response/LoginResponse.kt (1)

5-10: 일관성을 위해 @SerialName 어노테이션 추가를 권장합니다.

다른 응답 모델들(AuthResponse, PostCreateRequest 등)은 모두 @SerialName 어노테이션을 사용하고 있습니다. 일관성과 명시성을 위해 이 클래스에도 추가하는 것이 좋습니다.

다음 diff를 적용하세요:

 @Serializable
 data class LoginResponse(
-    val token: String,
-    val userId: Long,
-    val username: String
+    @SerialName("token") val token: String,
+    @SerialName("userId") val userId: Long,
+    @SerialName("username") val username: String
 )
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/UriUtils.kt (1)

10-26: URI를 파일로 변환하는 로직이 적절합니다.

리소스 관리를 위해 use 블록을 사용하고 예외 처리도 포함되어 있습니다. 다만, 캐시 디렉토리에 생성된 임시 파일들이 누적될 수 있으니 주기적인 정리 메커니즘을 고려해보세요.

필요시 캐시 정리 로직 추가를 고려하세요:

fun clearCache(context: Context) {
    context.cacheDir.listFiles()?.forEach { file ->
        if (file.name.startsWith("image_") && file.lastModified() < System.currentTimeMillis() - 86400000) {
            file.delete()
        }
    }
}
app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepository.kt (1)

6-20: TokenRepository 시그니처에서 Context 의존성 최소화 제안

지금 인터페이스가 대부분 Context 를 파라미터로 받는데, 이렇게 되면 Repository 계층이 Android 프레임워크에 강하게 결합됩니다. 가능하다면 DataStoreApplicationContext 를 생성자 주입해서, 메서드 시그니처에서 Context 를 없애는 구조도 한 번 고려해 보시면 좋겠습니다.

특히 validateToken(context: Context) 는 현재 구현( TokenRepositoryImpl )에서 context 를 전혀 사용하지 않으므로, 실제로 사용할 계획이 없다면 파라미터를 제거하거나(또는 나중에 토큰 삭제 등에서 사용할 예정이면 주석으로 의도를 남겨두는 것)을 추천드립니다.

app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt (2)

14-17: DataStore 확장 프로퍼티/키는 구현 상세이므로 가급적 캡슐화하면 좋습니다

Context.datastore, KEY, AUTO_LOGIN_KEY 가 파일 전역 public 으로 열려 있어 다른 레이어에서 직접 접근할 수 있는 상태입니다. 지금 PR 범위에서는 이 파일 안에서만 쓰이는 구현 상세이므로, 다음처럼 private 로 한정해 두면 추후 구조 변경 시 영향 범위를 줄이기 좋습니다.

-private val Context.datastore: DataStore<Preferences> by preferencesDataStore("Token")
-val KEY = stringPreferencesKey("token")
-// 자동 로그인 정보 키
-val AUTO_LOGIN_KEY = booleanPreferencesKey("auto_login")
+private val Context.datastore: DataStore<Preferences> by preferencesDataStore("Token")
+private val KEY = stringPreferencesKey("token")
+// 자동 로그인 정보 키
+private val AUTO_LOGIN_KEY = booleanPreferencesKey("auto_login")

(이미 다른 파일에서 KEY / AUTO_LOGIN_KEY 를 직접 참조 중이라면 그 사용처도 함께 조정이 필요합니다.)


41-72: validateToken 구현 단순화 및 Context 활용 여부 정리 제안

validateToken(context: Context) 가 내부에서 context 를 사용하지 않고, Result<BaseResponse<Boolean>> 를 다시 Result<Boolean> 으로 감싸는 로직이 조금 복잡하게 표현되어 있습니다.

  1. Context 파라미터

    • 현재 구현에서는 context 를 전혀 사용하지 않으므로, 사용 계획이 없다면 시그니처에서 제거하는 편이 더 명확합니다.
    • 반대로, 토큰이 유효하지 않을 때 deleteToken(context) 까지 같이 호출할 의도라면, 그 로직을 이 함수 안에 넣어 두면 호출 측에서 사용하기 더 편해집니다.
  2. Result 변환 로직 단순화

    • fold 대신 mapCatching 을 사용하면 조금 더 읽기 쉬운 형태로 바꿀 수 있습니다. 예를 들면:
override suspend fun validateToken(context: Context): Result<Boolean> {
    return getValidateTokenApi().mapCatching { baseResponse ->
        if (baseResponse.data == true) {
            true
        } else {
            throw Exception(baseResponse.message ?: "토큰 검증 실패")
        }
    }
}

동작은 동일하면서도 성공/실패 흐름이 한눈에 들어옵니다.

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

72-75: "refreshNeeded" 문자열을 상수로 추출하면 재사용성과 안전성이 좋아집니다

onPostCreatedonPostUpdated 양쪽에서 "refreshNeeded" 키를 그대로 문자열로 사용하고 있어, 오타나 키 변경 시 추적이 어려울 수 있습니다. 간단히 상수로 빼 두면 관리가 수월합니다.

예시:

// 이 파일 상단에 추가
private const val REFRESH_NEEDED = "refreshNeeded"

그리고 사용처는 다음처럼 변경할 수 있습니다.

-                        ?.set("refreshNeeded", true)
+                        ?.set(REFRESH_NEEDED, true)

두 군데 모두 동일하게 적용하시면 됩니다.

Also applies to: 94-97

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

6-18: App 싱글톤 및 DI 컨테이너 노출 방식 관련

App.instanceprivate set으로 보호되어 있는데, containerlateinit var로 외부에서 재할당이 가능합니다. DI 컨테이너는 보통 앱 생명주기 동안 불변으로 두는 편이어서,

lateinit var container: AppContainer
    private set

처럼 setter를 private으로 두거나 val + 지연 초기화를 고려해 볼 만합니다.

app/build.gradle.kts (1)

23-30: BASE_URL 누락 시 조기 실패(fail-fast) 처리 권장

local.propertiesBASE_URL이 없으면 baseUrlnull이 되고, 현재 설정대로면 BuildConfig.BASE_URL 값이 "null" 문자열이 됩니다. 빌드는 되지만, 런타임에 이상한 URL로 호출될 수 있어 디버깅이 어려워집니다.

예를 들어 다음처럼 값이 없을 때는 빌드를 실패시키거나 명시적으로 에러를 내는 걸 고려해 보세요.

val baseUrl = properties.getProperty("BASE_URL")
    ?: error("BASE_URL is not defined in local.properties")

buildConfigField("String", "BASE_URL", "\"$baseUrl\"")

이렇게 하면 설정 실수가 바로 드러나서 운영 안정성이 좋아집니다.

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

3-13: 이미지 업로드 성공 상태의 Map<String, String> 타입 개선 여지

Success에서 imgUrl: Map<String, String>을 사용하는 건 유연하지만, 키 값 정의가 느슨해서 호출부에서 실수를 하기 쉽습니다.
향후 스펙이 고정적이라면,

data class UploadImageResult(val url: String)

같은 전용 모델 또는 typealias ImgUrlMap = Map<String, String>처럼 의미를 드러내는 래핑을 고려해 보셔도 좋겠습니다.

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

9-28: AuthInterceptor에서 runBlocking + Context 사용 방식에 대한 코멘트

현재 구현은 기능적으로는 잘 동작할 것으로 보입니다만, 두 가지 정도만 참고하시면 좋겠습니다.

  1. Context 타입 명시
    인터셉터는 보통 앱 전체에서 오래 살아 있기 때문에, ActivityFragment 컨텍스트를 넘기면 메모리 릭 위험이 있습니다. 생성 시 반드시 applicationContext를 넘기도록 규약을 명시하거나, 생성자에서 context.applicationContext로 한 번 감싸 두는 것도 안전합니다.

  2. runBlocking 비용
    OkHttp 인터셉터는 원래 동기라 runBlocking 자체는 허용되지만, 매 요청마다 DataStore에서 토큰을 읽게 되면 약간의 오버헤드가 있습니다.

    • DataStore가 내부 캐시를 쓰기 때문에 실제 IO 비용은 크지 않겠지만,
    • 토큰이 자주 바뀌지 않는다면 TokenRepository 쪽에서 메모리 캐시를 두는 식으로 최적화할 여지도 있습니다.

현재 PR 범위에서는 크게 막을 정도의 이슈는 아니고, 추후 트래픽/성능 요구가 늘어날 때 고려할 수 있는 리팩토링 포인트로 보입니다.

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

91-91: 불필요한 trailing comma 제거

Kotlin에서는 함수 호출 시 마지막 인자 뒤의 trailing comma가 불필요합니다.

-viewModel.login(context,)
+viewModel.login(context)

-viewModel.signup(context,)
+viewModel.signup(context)

-viewModel.getToken(context,)
+viewModel.getToken(context)

Also applies to: 96-96, 103-103

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

7-25: 업로드/작성 API 응답 타입을 전용 모델로 통일하는 것을 고려해보세요.

현재 uploadImageResult<Map<String, String>>를 반환하고, createPostauthor: String 파라미터를 필수로 받습니다.

  • 이미지 업로드는 Map<String, String> 대신 ImageUploadResponse 같은 전용 응답 모델을 사용하면 키 이름 변경 등에 더 안전해지고, 사용처에서 의미를 더 명확히 표현할 수 있습니다.
  • createPostauthorApiService.createPost에서 기본값("규빈")을 사용하고 있어, 레포지토리/서비스 간에 작성자 처리 전략(항상 명시 전달 vs 서버 기본값 사용)을 통일해 두면 나중에 변경 시 혼란을 줄일 수 있습니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt (2)

66-85: 삭제 결과 처리 시 사용자 피드백을 조금 더 보완하면 좋겠습니다.

LaunchedEffect(deleteUiState)에서 성공 시 onNavigateBack()으로 잘 빠져나가지만, 실패(DeletePostUiState.Error)일 때는 로그만 남기고 UI에는 아무 변화가 없습니다. 최소한 스낵바/다이얼로그/텍스트 등으로 “삭제 실패”를 알려 주면 사용자가 재시도 여부를 판단하기 쉬울 것 같습니다.
또한 네트워크 오류 등 일시적인 실패와 서버 측 비즈니스 오류를 구분할 수 있다면, 삭제 재시도 버튼을 제공하는 것도 고려해볼 만합니다.


107-217: 상태별 UI에서 로딩/에러 표현과 Success 분기 캐스팅을 정리하면 가독성과 UX가 좋아집니다.

  • PostDetailUiState.Loading 분기가 비어 있어, 상세 진입 시 잠깐이라도 완전히 빈 화면이 보일 수 있습니다. Box(Modifier.fillMaxSize().padding(paddingValues)) 안에 CircularProgressIndicator()를 배치해 최소한의 로딩 표시를 해주는 편이 좋습니다.
  • PostDetailUiState.Error에서도 현재 Text("로딩 실패")만 있고 padding(paddingValues)를 사용하지 않아 AppBar 아래에 겹칠 수 있습니다. Success 분기처럼 Modifier.padding(paddingValues)를 적용해 레이아웃 일관성을 맞추는 것을 권장합니다.
  • Success 분기 내부에서 (uiState as PostDetailUiState.Success) 캐스트가 여러 번 반복되고 있어 읽기 어렵습니다. 분기 시작부에서 val success = uiState as PostDetailUiState.Success로 한 번만 캐스팅한 뒤 success.post.xxx로 사용하는 식으로 정리하면, 코드가 더 명확해지고 향후 상태 변경 시에도 안전하게 유지보수할 수 있습니다.
when (val state = uiState) {
    is PostDetailUiState.Success -> {
        val post = state.post
        // post.author.profileImageUrl, post.title, post.content 등 사용
    }
    ...
}
app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt (1)

19-68: Retrofit 선언은 적절하지만, 업로드 응답 타입을 전용 모델로 두는 편이 더 안전합니다.

전체적으로 HTTP 메서드/경로/어노테이션 조합은 자연스럽습니다. 다만 uploadImage에서 BaseResponse<Map<String, String>>를 사용하는 대신, 도메인 전용 응답 DTO(ImageUploadResponse 등)를 도입해 서버 스키마를 명시적으로 캡슐화하는 것을 권장합니다. 이렇게 하면 키 이름 변경이나 필드 추가가 발생했을 때, Map을 직접 다루는 코드 곳곳을 수정하는 대신 DTO를 중심으로 변경 범위를 좁힐 수 있습니다.

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

72-97: 목록 UI 상태 분기와 posts 스냅샷 추출 패턴이 적절하게 설계되었습니다.

when (uiState)에서 PostListUiState.Success 분기 안에서 val posts = (uiState as PostListUiState.Success).posts로 한 번만 스냅샷을 추출하고, LazyColumn 안에서는 posts만 캡처하도록 분리한 방식이 좋습니다. 이전 Composition의 uiState를 캡처해서 ClassCastException이 나는 패턴을 피하면서도, 코드 가독성도 확보되었습니다.
다만 로딩 상태에서는 CircularProgressIndicator()Modifier.padding(paddingValues)Box(Modifier.fillMaxSize())를 같이 써서 중앙 정렬을 해주고, 에러 상태에서도 동일하게 패딩을 적용하면 화면 전반의 일관성이 더 좋아질 것 같습니다.

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

38-62: 회원가입/로그인 실패 시 UI 상태 업데이트가 없어서 UX 가 빈약해질 수 있습니다.

signup/login에서 ResultonSuccess만 처리하고 onFailure에서 아무 작업도 하지 않아, 네트워크 오류나 잘못된 아이디/비밀번호 같은 실패 상황이 UI에 전혀 전달되지 않습니다.
LoginUiStateerrorMessageloginResult 같은 필드를 추가해 실패 시에도 _uiState.update { ... }로 상태를 갱신하고, 화면에서 토스트/다이얼로그/텍스트로 피드백을 보여주는 흐름을 붙여두면 실제 사용 환경에서 디버깅과 UX 모두 훨씬 좋아집니다.


72-131: 토큰 검증 상태 모델링을 조금 더 명확히 하면 추후 확장과 디버깅에 도움이 됩니다.

현재 validateToken에서 검증 시작 시 isLoading = true와 함께 tokenValidationState = TokenValidationState.Initial로 되돌리고, 이후 성공/실패에 따라 Success/Failure를 설정하고 있습니다.

  • Initial이 “앱 최초 상태”와 “검증 중 UI 전환용 임시 상태” 두 역할을 동시에 가져서, 화면 쪽 분기 로직이 복잡해질 여지가 있습니다. TokenValidationStateLoading을 추가하거나, 검증 중에는 isLoading만으로 UI를 제어하고 tokenValidationState는 그대로 두는 쪽이 더 표현력이 좋습니다.
  • onFailure에서는 Failure로만 두고 토큰/자동 로그인을 그대로 유지하는데, 이 경우 UI에서는 “네트워크 오류”와 “실제 토큰 만료/무효”를 동일한 Failure로 보게 됩니다. 필요하다면 Failure.Network/Failure.InvalidToken처럼 세분화하거나, 최소한 에러 메시지를 함께 상태에 올려 화면에서 구분해 줄 수 있도록 하는 것도 고려해볼 만합니다.

현재 로직 자체는 요구사항(유효하지 않은 토큰 시 삭제 + 자동 로그인 해제)을 잘 충족하고 있어, 위 제안은 상태 표현을 더 명확히 하려는 차원의 개선 사항입니다.

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

9-91: 에러 로그에 throwable을 함께 넘기면 디버깅이 더 쉬워집니다.

현재는 Log.e("PostRepository", error.message.toString())로 메시지만 남기고 있어 스택트레이스를 잃습니다. 아래처럼 throwable을 함께 넘기면 추후 서버/파싱 이슈 분석이 더 수월합니다.

}.onFailure { error ->
    Log.e("PostRepository", "getPosts 실패", error)
}

다른 메서드(createPost, getPostDetail, updatePost, deletePost, uploadImage)에도 동일 패턴을 적용하는 것을 고려해 보시면 좋겠습니다.

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

76-81: isLoadedpostId에 종속시키면 재사용 시 안정성이 좋아집니다.

현재 var isLoaded by remember { mutableStateOf(false) }로 선언되어 있어, 같은 컴포저블 인스턴스를 다른 postId로 재사용하게 되면 이전 게시글의 title/content가 남아 있을 수 있습니다.

아래처럼 postId에 의존하도록 기억 범위를 조정하면 더 안전합니다.

var isLoaded by remember(postId) { mutableStateOf(false) }

이렇게 하면 다른 postId로 진입할 때 자동으로 초기화됩니다.

📜 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 f61c8b3.

📒 Files selected for processing (51)
  • .coderabbit.yaml (1 hunks)
  • .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/AuthInterceptor.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/LoginRequest.kt (1 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/LoginResponse.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/LoginRepository.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.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/data/repository/TokenRepository.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (5 hunks)
  • app/src/main/java/com/example/kuit6_android_api/ui/navigation/Routes.kt (1 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/LoginScreen.kt (1 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/LoginUiState.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/TokenValidationState.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/LoginViewModel.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModelFactory.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 (3 hunks)
💤 Files with no reviewable changes (3)
  • app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt
  • app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
  • CLAUDE.md
🔇 Additional comments (36)
app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthResponse.kt (1)

6-11: LGTM!

직렬화 어노테이션이 올바르게 적용되어 있고, 필드 타입도 적절합니다.

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

6-11: LGTM!

요청 모델이 올바르게 구성되어 있으며, 선택적 imageUrl 필드도 적절합니다.

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

6-10: LGTM!

로그인 요청 모델이 올바르게 정의되어 있습니다.

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

6-9: LGTM!

이미지 업로드 응답 모델이 올바르게 구성되어 있습니다.

.idea/gradle.xml (1)

12-13: LGTM!

Gradle 모듈 경로가 프로젝트 루트를 기준으로 올바르게 설정되었습니다.

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

6-12: LGTM!

제네릭 타입을 사용한 API 응답 래퍼가 잘 설계되었습니다. 모든 필드에 적절한 직렬화 어노테이션이 적용되어 있으며, timestamp의 기본값 처리도 적절합니다.

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

3-7: 깔끔한 상태 패턴 구현입니다.

토큰 검증의 세 가지 상태를 명확하게 표현하고 있으며, sealed class와 object를 적절히 사용했습니다.

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

3-11: 로그인 UI 상태가 잘 구성되어 있습니다.

토큰 검증 상태와 로딩 상태를 포함하여 자동 로그인 기능에 필요한 모든 필드가 적절하게 정의되었습니다.

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

5-15: 일관된 UI 상태 패턴이 잘 적용되었습니다.

Loading, Success, Error 상태가 명확하게 정의되었고, Success 상태에서 PostResponse를 반환하는 것이 적절합니다.

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

28-40: 파일명 추출 로직이 안전하게 구현되었습니다.

ContentResolver 쿼리와 커서 처리가 적절하며, nameIndex 검증도 포함되어 있습니다.

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

5-15: 포스트 상세 화면의 UI 상태가 잘 정의되었습니다.

다른 UI 상태 클래스들과 일관된 패턴을 따르고 있어 코드베이스 전체의 일관성이 유지됩니다.

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

6-15: API 응답 모델이 올바르게 정의되었습니다.

모든 필드에 @SerialName 어노테이션이 적용되었고, nullable 필드(imageUrl)도 적절하게 처리되었습니다.

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

32-33: 응답 모델 타입으로의 마이그레이션이 깔끔하게 완료되었습니다.

Post에서 PostResponse로의 타입 변경이 일관되게 적용되었고, 컴포넌트의 동작에는 영향이 없습니다.

Also applies to: 38-38


156-164: 프리뷰가 새로운 응답 모델에 맞게 업데이트되었습니다.

PostResponseAuthorResponse를 사용하여 프리뷰 데이터를 생성하고 있으며, 모든 필수 필드가 포함되어 있습니다.

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

16-27: 커스텀 Application 등록 확인

android:name=".App" 으로 커스텀 Application 을 등록한 부분은 구성 상 자연해 보입니다. 다만 다음 두 가지는 한 번만 확인해 주세요.

  • App 클래스가 android.app.Application (또는 그 하위 타입, 예: Hilt 사용 시 HiltAndroidApp 기반 클래스) 를 상속하고 있는지
  • AndroidManifest.xmlpackageApp 클래스의 패키지(com.example.kuit6_android_api.App) 가 일치하는지

문제 없다면 이 변경 자체는 그대로 사용하셔도 될 것 같습니다.

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

4-20: 네비게이션 + ViewModel 팩토리 연결 구조 괜찮습니다

postViewModelFactory / loginViewModelFactory 를 이용해 각 Route 별로 필요한 Repository 를 주입하는 패턴이 잘 맞춰져 있고, onLoginClick 으로 LoginRoute 로 네비게이션하는 흐름도 자연스럽습니다. 현재 구조 기준으로는 특별히 막히는 부분은 없어 보입니다.

Also applies to: 31-44

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

5-15: PostListUiState 설계는 깔끔합니다 (data object 사용 시 Kotlin 버전만 확인)

Loading / Success / Error 로 나눈 sealed class 구조가 UI 상태 표현에 잘 맞고, Loadingdata object 로 선언한 것도 일관성 면에서 좋습니다.

다만 data object 는 비교적 최신 Kotlin 버전(1.9+ 이후)에서 지원되는 기능이므로, 프로젝트의 Kotlin 플러그인 버전이 이를 지원하는지 한 번만 확인해 주세요. 만약 호환성 문제가 있다면 data object Loading 을 그냥 object Loading 으로 바꾸는 것만으로도 동일한 용도로 사용 가능합니다.

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

5-18: 타입 세이프 네비게이션용 Route 정의 적절

PostEditRouteLoginRoute 모두 @Serializable로 처리되어 있고, 기존 패턴(object/data class)과 일관성 있게 잘 맞습니다. 네이밍과 구조도 직관적이라 유지보수에 무리 없을 것 같습니다.

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

10-19: ViewModelFactory에서 App 캐스팅 시 매니페스트 설정 확인 필요

this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as App 캐스팅은 AndroidManifest.xmlandroid:name=".App" 이 제대로 등록되어 있을 때만 안전합니다.
해당 설정만 확인되면, postRepository를 DI하는 이 패턴은 재사용성과 테스트 용이성 측면에서 잘 잡힌 것 같습니다.

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

5-15: PostCreate UI 상태 모델링 깔끔함

Loading / Success(PostResponse) / Error(message)로 나눈 sealed class 구조가 직관적이고, ViewModel–UI 간 상태 전달에도 무리가 없어 보입니다. 이후 에러 타입이 늘어나면 Error를 서브타입으로 확장하는 것도 자연스러울 것 같습니다.

app/build.gradle.kts (1)

81-93: DataStore / Retrofit / OkHttp 의존성 추가 방향 적절

토큰 저장/로딩(DataStore)과 API 통신(Retrofit + OkHttp + logging interceptor)을 위해 필요한 의존성 구성이 잘 되어 있습니다. 나중에 네트워크 레벨 테스트를 위해서는 OkHttp mockWebServer 같은 테스트 의존성 추가도 고려해 볼 수 있을 것 같습니다.

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

12-27: 간단한 수동 DI 컨테이너 구성 잘 되어 있음

RetrofitClient.apiService를 기준으로 PostRepository, LoginRepository, TokenRepository를 lazy로 주입하는 구조가 명료하고, App에서 한 번만 생성해 앱 전역으로 쓰기에도 적절해 보입니다.
향후 의존성이 더 늘어나면 모듈별로 분리하거나 DI 프레임워크(Hilt 등)로 이전하는 것도 자연스러운 다음 단계일 것 같습니다.

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

5-15: 인터페이스 정의가 깔끔합니다.

저장소 패턴을 잘 따르고 있으며, Result 타입을 사용한 에러 핸들링과 suspend 함수 사용이 적절합니다.

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

15-26: Result 타입을 사용한 에러 핸들링이 적절합니다.

runCatchingResult 타입을 사용한 에러 핸들링 패턴이 좋습니다. 응답 검증 로직도 명확합니다.

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

34-37: Kotlinx Serialization 설정이 적절합니다.

ignoreUnknownKeyscoerceInputValues 설정으로 서버 응답의 유연한 처리가 가능합니다.

app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt (2)

45-47: 자동 로그인 초기화 로직이 적절합니다.

LaunchedEffect(Unit)을 사용하여 화면 진입 시 한 번만 자동 로그인을 초기화하는 패턴이 올바릅니다.


38-42: 토큰 검증 상태에 따른 버튼 텍스트 업데이트가 잘 구현되었습니다.

when 표현식을 사용한 상태별 텍스트 분기 처리가 명확합니다.

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

11-22: ViewModel Factory 구현이 우수합니다.

reified 타입 파라미터와 inline 함수를 사용한 제네릭 팩토리 패턴이 매우 깔끔합니다. AppContainer에서 의존성을 주입받는 구조도 적절합니다.

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

15-24: UI 상태 관리가 잘 구조화되어 있습니다.

게시글 작성과 이미지 업로드를 별도의 StateFlow로 분리하여 관리하는 것이 좋은 설계입니다. asStateFlow()를 통한 읽기 전용 노출도 적절합니다.


27-45: 게시글 작성 로직이 견고합니다.

로딩 상태 설정, 저장소 호출, 성공/실패 처리가 모두 적절하게 구현되어 있습니다.


48-64: 이미지 업로드 에러 핸들링이 적절합니다.

MultipartBody.Part를 사용한 파일 업로드와 에러 메시지 전달이 잘 구현되어 있습니다.

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

12-21: ViewModel 초기화 패턴이 우수합니다.

init 블록에서 데이터를 로드하는 패턴이 적절하며, StateFlow를 사용한 상태 관리가 깔끔합니다.


23-36: 게시글 목록 로드 로직이 견고합니다.

로딩 상태 관리와 에러 핸들링이 PostCreateViewModel과 일관된 패턴을 따르고 있어 좋습니다.


38-40: refresh 기능 제공이 좋습니다.

사용자가 수동으로 목록을 새로고침할 수 있는 기능을 제공하는 것이 좋은 UX입니다.

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

13-40: Post 상세 로딩/삭제 StateFlow 설계가 명확합니다.

ResultPostDetailUiState/DeletePostUiState로 바로 매핑하고, viewModelScope로 생명주기도 잘 맞춰서 관리하고 있어 특별한 문제는 없어 보입니다.

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

15-82: 수정/조회/이미지 업로드용 UiState 분리가 잘 되어 있습니다.

editPost, getPostDetail, uploadImage 각각을 코루틴 + Result → UiState로 매핑하는 구조가 일관적이고, ViewModel 책임 범위도 잘 맞춰져 있어 그대로 사용해도 무방해 보입니다.

Comment thread .coderabbit.yaml Outdated
version: 1

reviews:
auto_review: 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 | 🔴 Critical

설정 구문 오류를 수정하세요.

auto_review는 boolean 값이 아닌 객체여야 합니다. 현재 설정으로는 파싱 오류가 발생하여 CodeRabbit이 작동하지 않습니다.

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

 reviews:
-  auto_review: true              # 자동 리뷰 활성화
+  auto_review:
+    enabled: true                # 자동 리뷰 활성화
+    auto_incremental_review: true # 증분 리뷰 활성화
   review_status: true            # 체크 메시지 표시
📝 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
auto_review: true # 자동 리뷰 활성화
reviews:
auto_review:
enabled: true # 자동 리뷰 활성화
auto_incremental_review: true # 증분 리뷰 활성화
review_status: true # 체크 메시지 표시
🤖 Prompt for AI Agents
In .coderabbit.yaml around line 4, the auto_review key is currently a bare
boolean which causes a parsing error; change it to an object structure (for
example include an "enabled" key and any other expected options) instead of
true/false, and ensure proper YAML indentation and formatting so the file parses
(replace "auto_review: true" with an object mapping like auto_review: { enabled:
true, ... } or equivalent multi-line mapping).

Comment thread .coderabbit.yaml Outdated
Comment on lines +9 to +15
triggers:
pull_request: true # PR 생성 시 자동 리뷰
pull_request_branches: # 어떤 브랜치든지 허용
include:
- "*" # 모든 브랜치
push: false
pull_request_review: false

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

triggers 설정은 auto_review 객체 내부로 이동해야 합니다.

스키마에 따르면 triggers 관련 설정(pull_request, push 등)은 더 이상 최상위 속성이 아닙니다. 대신 auto_review 객체 내의 base_branchesdrafts 속성을 사용하거나 제거해야 합니다.

다음과 같이 수정하세요:

   auto_review:
     enabled: true                # 자동 리뷰 활성화
     auto_incremental_review: true
+    base_branches:
+      - ".*"                     # 모든 브랜치에서 자동 리뷰
+    drafts: false                # 드래프트 PR은 제외
   review_status: true
   max_review_comments: 50
-
-  # 모든 브랜치 PR에 대해 자동으로 리뷰
-  triggers:
-    pull_request: true           # PR 생성 시 자동 리뷰
-    pull_request_branches:       # 어떤 브랜치든지 허용
-      include:
-        - "*"                    # 모든 브랜치
-    push: false
-    pull_request_review: false
📝 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
triggers:
pull_request: true # PR 생성 시 자동 리뷰
pull_request_branches: # 어떤 브랜치든지 허용
include:
- "*" # 모든 브랜치
push: false
pull_request_review: false
auto_review:
enabled: true # 자동 리뷰 활성화
auto_incremental_review: true
base_branches:
- ".*" # 모든 브랜치에서 자동 리뷰
drafts: false # 드래프트 PR은 제외
review_status: true
max_review_comments: 50
🤖 Prompt for AI Agents
.coderabbit.yaml around lines 9 to 15: the current top-level triggers block must
be moved into the auto_review object per schema; remove the top-level triggers
and instead add equivalent settings under auto_review using base_branches (to
represent branch inclusion patterns) and drafts (if draft PR handling is
required), or drop unsupported keys; ensure the resulting auto_review object
contains base_branches: ["*"] (or the intended branch list) and any
draft-related flags, and validate the YAML against the schema to confirm no
remaining top-level trigger keys.

Comment on lines +20 to +24
@SuppressLint("StaticFieldLeak")
private val authInterceptor = AuthInterceptor(
App.instance.applicationContext,
TokenRepositoryImpl()
)

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 | 🔴 Critical

아키텍처: TokenRepositoryImpl의 직접 인스턴스화 문제

AuthInterceptor에서 TokenRepositoryImpl()을 직접 생성하면 다음 문제가 발생합니다:

  • AppContainer의 tokenRepository와 다른 인스턴스가 생성됨
  • 싱글톤이 보장되지 않아 상태 불일치 가능
  • 의존성 주입 원칙 위반

AppContainer에서 공유 인스턴스를 주입받도록 수정하세요:

+// RetrofitClient를 object에서 class로 변경하고 AppContainer에서 초기화
-object RetrofitClient {
+class RetrofitClient(tokenRepository: TokenRepository) {
     private val loggingInterceptor = HttpLoggingInterceptor().apply {
         level = HttpLoggingInterceptor.Level.BODY
     }
 
-    @SuppressLint("StaticFieldLeak")
-    private val authInterceptor = AuthInterceptor(
-        App.instance.applicationContext,
-        TokenRepositoryImpl()
-    )
+    private val authInterceptor = AuthInterceptor(
+        App.instance.applicationContext,
+        tokenRepository
+    )

그리고 AppContainer에서:

val tokenRepository: TokenRepository by lazy { TokenRepositoryImpl() }
val retrofitClient by lazy { RetrofitClient(tokenRepository) }
val apiService: ApiService by lazy { retrofitClient.apiService }
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt
around lines 20 to 24, you're directly instantiating TokenRepositoryImpl for the
AuthInterceptor which causes multiple instances and breaks DI/singleton
guarantees; change RetrofitClient to accept a TokenRepository (interface) via
constructor and use that instance to create AuthInterceptor instead of new
TokenRepositoryImpl(), then update AppContainer to provide a single
TokenRepository by lazy and to construct RetrofitClient(tokenRepository) so the
shared AppContainer instance is injected everywhere.

Comment on lines +23 to +25
}.onFailure { error ->
Log.e("LoginRepository", error.message.toString())
}

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

보안: 에러 메시지 로깅 시 민감 정보 노출 위험

error.message를 직접 로깅하면 사용자 ID, 비밀번호 등의 민감한 정보가 로그에 포함될 수 있습니다. 프로덕션 빌드에서는 민감 정보를 제거하거나 로깅을 비활성화해야 합니다.

다음과 같이 수정하는 것을 권장합니다:

 }.onFailure { error ->
-    Log.e("LoginRepository", error.message.toString())
+    Log.e("LoginRepository", "Signup failed", 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
}.onFailure { error ->
Log.e("LoginRepository", error.message.toString())
}
}.onFailure { error ->
Log.e("LoginRepository", "Signup failed", error)
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt
around lines 23 to 25, the current onFailure handler logs error.message directly
which can leak sensitive data; replace this by not logging raw error messages in
production: log a sanitized/generic message (e.g., "Login failed") or the error
type only, and conditionally emit detailed logs only for debug builds
(BuildConfig.DEBUG) or route full error details to a secure internal
error-collection system instead of standard logs; ensure no user identifiers,
passwords, or full exception messages are written to logcat in release builds.

Comment on lines +40 to +42
}.onFailure { error ->
Log.e("LoginRepository", error.message.toString())
}

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

보안: 에러 메시지 로깅 시 민감 정보 노출 위험

signup 함수와 동일한 보안 이슈가 있습니다. 에러 메시지에 민감 정보가 포함될 수 있습니다.

다음과 같이 수정하세요:

 }.onFailure { error ->
-    Log.e("LoginRepository", error.message.toString())
+    Log.e("LoginRepository", "Login failed", 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
}.onFailure { error ->
Log.e("LoginRepository", error.message.toString())
}
}.onFailure { error ->
Log.e("LoginRepository", "Login failed", error)
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt
around lines 40 to 42, the current onFailure block logs error.message directly
which may expose sensitive information; replace direct error.message logging
with a generic log entry (e.g., "Login failed") and, if you need the full error
for diagnostics, send it to a secure telemetry/crash-reporting system instead of
standard logs; ensure you handle null safely (avoid toString() on a potentially
null message) and remove any sensitive fields from what you report.

Text("회원가입")
}
}
Text("토큰 : ${uiState.token}")

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

보안: UI에 토큰 값 노출

액세스 토큰을 UI에 직접 표시하면 보안 위험이 있습니다:

  • 스크린샷/화면 녹화로 토큰 유출 가능
  • 어깨 너머 공격(shoulder surfing)에 취약
  • 디버그 목적이라면 프로덕션 빌드에서 제거 필요

프로덕션에서는 이 줄을 제거하거나 디버그 빌드에서만 표시하도록 수정하세요:

-Text("토큰 : ${uiState.token}")
+// Debug only - remove in production
+if (BuildConfig.DEBUG) {
+    Text("토큰 : ${uiState.token?.take(10)}...") // 일부만 표시
+}
📝 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
Text("토큰 : ${uiState.token}")
// Debug only - remove in production
if (BuildConfig.DEBUG) {
Text("토큰 : ${uiState.token?.take(10)}...") // 일부만 표시
}
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt
around line 101, the UI is directly displaying the access token which exposes
sensitive credentials; remove this Text line from production and instead
conditionally show it only in debug builds (e.g., guard with BuildConfig.DEBUG)
or replace it with a non-sensitive placeholder/masked value for UI display and
log sensitive details only to secure debug logs not shipped in release APKs.

Comment on lines +257 to +288
// 업로드된 이미지 미리보기
if (selectedImageUri != null && imgUiState !is UploadImageUiState.Loading) {
Spacer(modifier = Modifier.height(12.dp))
Box(
modifier = Modifier.fillMaxWidth()
) {
AsyncImage(
model = selectedImageUri,
contentDescription = "선택된 이미지",
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(12.dp)),
contentScale = ContentScale.Crop
)

IconButton(
onClick = {
selectedImageUri = null
viewModel.clearUploadedImageUrl()
},
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
) {
Icon(
Icons.Default.Close,
contentDescription = "이미지 제거",
tint = MaterialTheme.colorScheme.error
)
}
}

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

이미지를 제거해도 uploadedImageUrl이 유지되어 서버로 전송될 수 있습니다.

이미지 미리보기의 닫기 버튼에서:

onClick = {
    selectedImageUri = null
    viewModel.clearUploadedImageUrl()
}

만 호출하고 있어, uploadedImageUrl(로컬 상태)은 여전히 이전 업로드 결과 값을 유지합니다. 이후 사용자가 이미지를 제거한 상태에서 "작성하기"를 누르면, 의도와 달리 제거된 이미지 URL이 서버로 함께 전송됩니다.

아래처럼 로컬 상태도 함께 초기화해 주는 것이 안전합니다.

- onClick = {
-     selectedImageUri = null
-     viewModel.clearUploadedImageUrl()
- },
+ onClick = {
+     selectedImageUri = null
+     uploadedImageUrl = null
+     viewModel.clearUploadedImageUrl()
+ },

이렇게 하면 UI/서버 양쪽에서 "이미지 제거" 동작이 일치하게 됩니다.


🏁 Script executed:

#!/bin/bash
# Find PostCreateScreen.kt and examine uploadedImageUrl state management
fd -e kt -name "PostCreateScreen.kt" | head -5

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Search for uploadedImageUrl variable in PostCreateScreen
rg -n "uploadedImageUrl" --type=kt -C5 app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Find createPost function and understand the request payload
rg -n "createPost|uploadedImageUrl" --type=kt app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt -A3 -B3

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check clearUploadedImageUrl implementation in the ViewModel
rg -n "clearUploadedImageUrl" --type=kt -A5 -B2 app/src/main/java/com/example/kuit6_android_api/

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Look at the complete IconButton onClick handler and surrounding context
rg -n "Icons.Default.Close|contentDescription.*이미지 제거" --type=kt -A10 -B10 app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Find where onPostCreated is called and understand the flow after createPost
rg -n "onPostCreated|createPost\(" --type=kt -C7 app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Compare with PostEditScreen to see if similar pattern exists there
fd -e kt -name "PostEditScreen.kt" | xargs rg -n "uploadedImageUrl|clearUploadedImageUrl" -C5

Length of output: 18474


이미지를 제거해도 uploadedImageUrl이 유지되어 서버로 전송될 수 있습니다.

이미지 미리보기의 닫기 버튼에서 uploadedImageUrl(로컬 상태)을 초기화하지 않고 있습니다. PostEditScreen과 달리 selectedImageUri = null만 호출하고 있어, 사용자가 이미지를 제거한 상태에서 "작성하기"를 누르면 의도와 달리 제거된 이미지 URL이 서버로 함께 전송됩니다(라인 302에서 imageUrl = uploadedImageUrl 사용).

아래처럼 로컬 상태도 함께 초기화해 주는 것이 필요합니다.

                            IconButton(
                                onClick = {
                                    selectedImageUri = null
+                                   uploadedImageUrl = null
                                    viewModel.clearUploadedImageUrl()
                                },

이렇게 하면 PostEditScreen(라인 220)과 일치하게 되며, UI/서버 양쪽에서 "이미지 제거" 동작이 일치하게 됩니다.

🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt
around lines 257 to 288, the image close button only clears selectedImageUri but
does not clear the uploaded image URL state, so a removed image can still be
submitted; update the IconButton onClick to also clear the uploaded image URL
(call the ViewModel clearUploadedImageUrl() and/or set any local
uploadedImageUrl variable to null) so the local UI and ViewModel state match
PostEditScreen and a removed image is not sent to the server.

Comment on lines +248 to 285
// 수정 버튼
Button(
onClick = {
val finalImageUrl = uploadedImageUrl
val request = PostCreateRequest(
title = title,
content = content,
imageUrl = finalImageUrl
)
// 수정 버튼 누를 시 PostEditViewModel의 editPost() 호출
viewModel.editPost(postId, request)
onPostUpdated()
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = title.isNotBlank() && content.isNotBlank() && imgUiState !is UploadImageUiState.Loading,
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.primary
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text("갤러리에서 이미지 선택")
if (imgUiState is UploadImageUiState.Loading) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = MaterialTheme.colorScheme.onPrimary
)
Spacer(modifier = Modifier.width(8.dp))
Text("업로드 중...")
}
} else {
Text("수정하기", fontWeight = FontWeight.Bold)
}
}

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

수정 버튼이 API 성공 여부와 무관하게 바로 onPostUpdated()를 호출합니다.

현재는 viewModel.editPost(...) 직후 곧바로 onPostUpdated()를 호출하고 있어,

  • 수정 API가 실패해도 이전 화면으로 돌아가 버리고,
  • 네트워크가 느린 경우에도 성공 여부를 확인하기 전에 화면이 닫힐 수 있습니다.

수정 결과가 Success일 때만 내비게이션/콜백을 트리거하도록 UiState를 관찰하는 쪽이 더 안전합니다. 예를 들어(개념 예시):

var hasEditRequested by remember { mutableStateOf(false) }

Button(
    onClick = {
        hasEditRequested = true
        viewModel.editPost(postId, request)
    },
    ...
)

// editPost 호출 이후에만 Success를 감지해서 콜백 실행
LaunchedEffect(uiState, hasEditRequested) {
    if (hasEditRequested && uiState is PostEditUiState.Success) {
        hasEditRequested = false
        onPostUpdated()
    }
}

이렇게 하면 최초 상세 조회용 Success와 실제 수정 완료 Success를 구분할 수 있고, 실패 시에는 화면을 유지한 채 에러 UI를 보여줄 여지도 생깁니다.


검증을 위해 코드베이스를 탐색하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find PostEditViewModel file
fd -type f -name "*PostEditViewModel*"

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Examine PostEditScreen.kt to understand the complete context around the button
cat app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt

Length of output: 12221


🏁 Script executed:

#!/bin/bash
# Search for PostEditUiState definition
fd -type f -name "*.kt" | xargs rg -l "PostEditUiState" -A 5 -B 5

Length of output: 419



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine PostEditViewModel.kt
cat app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt

Length of output: 3055


🏁 Script executed:

#!/bin/bash
# Examine PostEditUiState.kt
cat app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt

Length of output: 359


수정 버튼이 API 성공 여부와 무관하게 바로 onPostUpdated()를 호출합니다.

viewModel.editPost(postId, request)는 비동기 코루틴이지만, 다음 줄의 onPostUpdated()가 즉시 실행되고 있습니다.
실제 API 응답을 기다리지 않으므로:

  • 수정 API가 실패해도 이전 화면으로 돌아가 버리고,
  • 네트워크가 느린 경우에도 성공 여부를 확인하기 전에 화면이 닫힐 수 있습니다.

수정 결과가 Success일 때만 내비게이션/콜백을 트리거하는 것이 필요합니다. 예를 들어:

var hasEditRequested by remember { mutableStateOf(false) }

Button(
    onClick = {
        hasEditRequested = true
        viewModel.editPost(postId, request)
    },
    ...
)

LaunchedEffect(uiState, hasEditRequested) {
    if (hasEditRequested && uiState is PostEditUiState.Success) {
        hasEditRequested = false
        onPostUpdated()
    }
}

추가로 isLoaded 플래그도 postId 변경 시 초기화되도록 개선하세요:

  • 현재: remember { mutableStateOf(false) }
  • 개선: remember(postId) { mutableStateOf(false) }
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt
around lines 248-285, the Button currently calls onPostUpdated() immediately
after viewModel.editPost(...), which triggers navigation regardless of the async
API result; change the flow so the button only requests the edit and sets a
local "edit requested" flag (e.g., hasEditRequested = true) then call
viewModel.editPost(postId, request) without invoking onPostUpdated(); add a
LaunchedEffect (or observe uiState) that watches hasEditRequested and the
ViewModel's uiState and calls onPostUpdated() only when hasEditRequested is true
and uiState indicates Success (resetting hasEditRequested afterward on both
success and error as appropriate); also update the isLoaded state initialization
to remember(postId) { mutableStateOf(false) } so it resets when postId changes.

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.

⚠️ Potential issue | 🟡 Minor

Success 상태에서 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, the Success variant currently stores a Unit property
unnecessarily; change it from a data class with a Unit field to a singleton
object (remove the deletePost property and declare Success as an object) so that
success is represented without carrying meaningless data.

Comment thread gradle/libs.versions.toml

# Coroutines
coroutines = "1.9.0"
datastorePreferences = "1.2.0-rc01"

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

🧩 Analysis chain

DataStore Preferences의 RC 버전 사용을 확인하세요.

1.2.0-rc01은 릴리스 후보(RC) 버전입니다. 프로덕션 환경에서는 안정(stable) 버전 사용을 권장합니다.

최신 안정 버전을 확인하려면 다음을 검색해 주세요:


🌐 Web query:

What is the latest stable version of androidx.datastore datastore-preferences?

💡 Result:

The latest stable release is androidx.datastore:datastore-preferences (stable) version 1.1.7. [1]

Sources:
[1] Android Developers — Jetpack DataStore release notes (shows stable release 1.1.7).


DataStore Preferences RC 버전을 안정 버전으로 업데이트하세요.

최신 안정 버전은 1.1.7입니다. 현재 설정된 1.2.0-rc01은 릴리스 후보 버전으로, datastorePreferences = "1.1.7"로 변경하여 프로덕션 환경에서 안정성을 확보하시기 바랍니다.

🤖 Prompt for AI Agents
In gradle/libs.versions.toml around line 32, the datastorePreferences entry is
set to the release-candidate version "1.2.0-rc01"; update it to the stable
production version by changing the value to "1.1.7" so the file reads
datastorePreferences = "1.1.7".

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