Skip to content

Conversation

@koongmai
Copy link

@koongmai koongmai commented Nov 20, 2025

Summary by CodeRabbit

새로운 기능

  • 로그인 및 회원가입 기능 추가
  • 자동 로그인 옵션 지원
  • 세션 토큰 자동 저장 및 관리
  • 이미지 업로드 기능 추가
  • 포스트 생성, 수정, 삭제 기능 개선

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

@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Walkthrough

애플리케이션에 의존성 주입 컨테이너, 인증 플로우, 토큰 관리 기능을 추가하고 단일 ViewModel을 여러 개의 특화된 ViewModel로 분리하는 대규모 아키텍처 개선 작업입니다. DataStore 기반 토큰 저장소, 로그인/회원가입 API 엔드포인트, UI 상태 관리 체계를 도입합니다.

Changes

Cohort / File(s) 변경 요약
의존성 및 라이브러리 설정
app/build.gradle.kts, gradle/libs.versions.toml
DataStore Preferences 라이브러리 의존성 추가 (v1.3.0-alpha01)
애플리케이션 초기화
app/src/main/AndroidManifest.xml, app/src/main/java/.../App.kt
커스텀 Application 클래스 생성 및 AppContainer 초기화
의존성 주입 컨테이너
app/src/main/java/.../data/di/AppContainer.kt
PostRepository, LoginRepository, TokenRepository를 lazy 초기화하는 DI 컨테이너 구현
API 서비스 확장
app/src/main/java/.../data/api/ApiService.kt
uploadImage, signup, login 엔드포인트 추가
인증 데이터 모델
app/src/main/java/.../data/model/request/LoginRequest.kt, app/src/main/java/.../data/model/response/LoginResponse.kt
LoginRequest, LoginResponse 데이터 클래스 추가
저장소 인터페이스
app/src/main/java/.../data/repository/LoginRepository.kt, app/src/main/java/.../data/repository/PostRepository.kt, app/src/main/java/.../data/repository/TokenRepository.kt
인증, 게시물, 토큰 관리 저장소 인터페이스 정의
저장소 구현
app/src/main/java/.../data/repository/LoginRepositoryImpl.kt, app/src/main/java/.../data/repository/PostRepositoryImpl.kt, app/src/main/java/.../data/repository/TokenRepositoryImpl.kt
DataStore를 활용한 저장소 구현 및 API 호출 처리
UI 상태 관리
app/src/main/java/.../ui/post/state/LoginUiState.kt, app/src/main/java/.../ui/post/state/PostListUiState.kt, app/src/main/java/.../ui/post/state/PostDetailUiState.kt, app/src/main/java/.../ui/post/state/PostEditUiState.kt, app/src/main/java/.../ui/post/state/PostCreateUiState.kt, app/src/main/java/.../ui/post/state/DeletePostUiState.kt, app/src/main/java/.../ui/post/state/UploadImageUiState.kt
로딩, 성공, 오류 상태를 표현하는 sealed 클래스 UI 상태 정의
ViewModel 구현
app/src/main/java/.../ui/post/viewmodel/LoginViewModel.kt, app/src/main/java/.../ui/post/viewmodel/PostListViewModel.kt, app/src/main/java/.../ui/post/viewmodel/PostDetailViewModel.kt, app/src/main/java/.../ui/post/viewmodel/PostEditViewModel.kt, app/src/main/java/.../ui/post/viewmodel/PostCreateViewModel.kt
특화된 ViewModel 구현 (기존 PostViewModel 제거)
ViewModel 팩토리
app/src/main/java/.../ui/post/viewmodel/LoginViewModelFactory.kt, app/src/main/java/.../ui/post/viewmodel/PostViewModelFactory.kt
DI 컨테이너에서 저장소를 주입하는 ViewModel 팩토리
UI 화면 구현
app/src/main/java/.../ui/post/screen/LoginScreen.kt, app/src/main/java/.../ui/post/screen/PostListScreen.kt, app/src/main/java/.../ui/post/screen/PostDetailScreen.kt, app/src/main/java/.../ui/post/screen/PostEditScreen.kt, app/src/main/java/.../ui/post/screen/PostCreateScreen.kt
상태 기반 UI 렌더링으로 재구성, 로그인 화면 추가
네비게이션 업데이트
app/src/main/java/.../ui/navigation/NavGraph.kt, app/src/main/java/.../ui/navigation/Routes.kt, app/src/main/java/.../MainActivity.kt
LoginRoute 추가, SnackbarHostState 제거, viewModel 주입 방식 개선

Sequence Diagram(s)

sequenceDiagram
    participant App as Application<br/>(onCreate)
    participant AppContainer as AppContainer<br/>(DI)
    participant Repo as Repository
    participant API as ApiService
    
    App->>AppContainer: initialize container
    AppContainer->>AppContainer: lazy init PostRepository
    AppContainer->>AppContainer: lazy init LoginRepository
    AppContainer->>AppContainer: lazy init TokenRepository
    
    Note over AppContainer: Each Repository<br/>holds ApiService reference
    
    AppContainer-->>App: container ready
Loading
sequenceDiagram
    participant UI as UI Screen
    participant VM as ViewModel
    participant Repo as Repository
    participant DS as DataStore
    participant API as ApiService
    
    UI->>VM: refresh() / loadDetail()
    activate VM
    VM->>VM: emit Loading state
    VM->>Repo: fetch data (runCatching)
    activate Repo
    Repo->>API: API call
    API-->>Repo: Response
    Repo->>Repo: validate response
    Note over Repo: Check success &<br/>non-null data
    Repo-->>VM: Result<Data>
    deactivate Repo
    
    alt Success
        VM->>VM: emit Success(data)
        UI->>UI: render with data
    else Error
        VM->>VM: emit Error(message)
        UI->>UI: render error state
    end
    deactivate VM
Loading
sequenceDiagram
    participant LoginUI as LoginScreen
    participant LoginVM as LoginViewModel
    participant AuthRepo as LoginRepository
    participant TokenRepo as TokenRepository
    participant API as ApiService
    participant DS as DataStore
    
    LoginUI->>LoginVM: login(context)
    activate LoginVM
    LoginVM->>AuthRepo: login(id, password)
    activate AuthRepo
    AuthRepo->>API: POST /api/auth/login
    API-->>AuthRepo: LoginResponse(token, userId, username)
    AuthRepo-->>LoginVM: Result<LoginResponse>
    deactivate AuthRepo
    
    alt Success
        LoginVM->>TokenRepo: saveToken(context, token)
        activate TokenRepo
        TokenRepo->>DS: save token
        DS-->>TokenRepo: saved
        deactivate TokenRepo
        LoginVM->>LoginVM: emit Success with token
    end
    deactivate LoginVM
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

주의가 필요한 영역:

  • AppContainer.kt: 의존성 주입 패턴 구현이 올바른지, 각 저장소의 lazy 초기화가 적절한지 확인
  • TokenRepositoryImpl.kt: DataStore 설정 및 토큰 저장/조회 로직, 코루틴 플로우 처리 검증
  • LoginRepository(Impl).kt: API 응답 검증 로직(success 플래그 및 null 체크), 예외 처리 및 로깅
  • PostRepository(Impl).kt: 기존 코드와의 일관성, 모든 엔드포인트의 응답 검증 및 오류 처리
  • UI 화면들: 각 화면이 새로운 ViewModel 및 UI 상태 패턴을 올바르게 사용하고 있는지, 로딩/오류 상태 처리 확인
  • ViewModel 팩토리: DI 컨테이너 접근 메커니즘과 generic 타입 처리의 안전성
  • NavGraph.kt: SnackbarHostState 제거로 인한 에러 피드백 메커니즘 변경 및 LoginRoute 통합

Possibly related PRs

Poem

🐰 토큰 저장소에 로그인 담고,

컨테이너에서 모두 주입하고,

상태 다발로 UI 표현하네~

ViewModel 분산으로 더욱 명확하고,

아키텍처 개선되니 참 좋구나! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive PR 제목은 한국어로 작성되었으며, 주제가 모호하고 구체적인 변경 사항을 설명하지 않습니다. PR 제목을 더 명확하고 구체적으로 작성하세요. 예: '로그인 기능 추가 및 저장소 아키텍처 리팩토링' 또는 '인증 및 토큰 관리 기능 구현'
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

@alswlekk
Copy link
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.

✨ 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
Copy link

coderabbitai bot commented Nov 20, 2025

@alswlekk I'll review the changes in this pull request 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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (2)
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt (1)

139-171: 게시글 생성 결과를 확인하지 않고 네비게이션하는 문제

Lines 147-148에서 viewModel.createPost()를 호출한 직후 onPostCreated()를 즉시 호출하고 있습니다. 이는 게시글 생성이 완료되기 전에 화면을 벗어나는 문제를 일으킵니다.

현재 uiState를 수집하고 있지만(Line 50) 네비게이션 트리거에 활용하지 않고 있습니다. 게시글 생성 성공 시에만 화면을 벗어나도록 수정해야 합니다.

// PostCreateScreen 내부에 LaunchedEffect 추가
LaunchedEffect(uiState) {
    if (uiState is PostCreateUiState.Success) {
        onPostCreated()
    }
}

// Button onClick에서 onPostCreated() 제거
Button(
    onClick = {
        val finalAuthor = author
        val request = PostCreateRequest(
            title = title,
            content = content,
            imageUrl = null
        )
        viewModel.createPost(finalAuthor, request)
        // onPostCreated() 제거
    },
    // ... 나머지 코드

에러 처리도 고려하여 Snackbar 등으로 사용자에게 피드백을 제공하는 것이 좋습니다.

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

200-220: deleteUiState를 관찰하고 성공 시에만 네비게이션을 수행해야 합니다

검증 결과, 리뷰 지적이 정확합니다. ViewModel에는 deleteUiState: StateFlow<DeletePostUiState>가 이미 구현되어 있으며 삭제 결과(Success/Error)를 추적하고 있습니다. 그러나 PostDetailScreen에서는 이 상태를 관찰하지 않고 있어서, 네트워크 요청의 실제 결과와 무관하게 즉시 네비게이션되는 문제가 발생합니다.

필요한 수정:

  • deleteUiStatecollectAsState()로 수집
  • LaunchedEffect에서 Success 상태만 감지하여 onNavigateBack() 호출
  • Error 상태 시 사용자에게 에러 메시지 표시 (토스트/스낵바)

현재 확인 버튼의 로직:

viewModel.deletePost(postId)
showDeleteDialog = false
onNavigateBack()

수정해야 할 로직:

  • deletePost() 호출만 실행
  • LaunchedEffect에서 deleteUiState를 감시하여 결과에 따라 처리
🧹 Nitpick comments (21)
app/src/main/java/com/example/kuit6_android_api/MainActivity.kt (1)

52-60: 사용되지 않는 SnackbarHostState 관련 코드를 제거하세요.

snackBarState를 생성하고 ScaffoldsnackbarHost를 구성하고 있지만, 이제 NavGraph에 전달되지 않아 실제로 사용되지 않습니다. 불필요한 코드는 제거하는 것이 좋습니다.

다음 diff를 적용하여 사용되지 않는 코드를 제거하세요:

-                val snackBarState = remember{ SnackbarHostState() }
-
                 Scaffold(
                     modifier = Modifier
                         .fillMaxSize()
-                        .background( MaterialTheme.colorScheme.background),
-                    snackbarHost= {
-                        SnackbarHost(hostState = snackBarState) //자동으로 UI를 띄울 공간을 만듦
-                    }
+                        .background( MaterialTheme.colorScheme.background)
                 ) {
app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt (1)

6-7: 코드 스타일 일관성을 위해 공백을 추가하세요.

등호(=) 앞에 공백이 누락되어 일관성이 떨어집니다.

다음 diff를 적용하여 스타일을 통일하세요:

-    val password: String= "",
-    val isAutoLogin: Boolean= false,
+    val password: String = "",
+    val isAutoLogin: Boolean = false,
app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt (2)

14-14: 불필요한 생성자 괄호를 제거하세요.

빈 생성자 괄호 ()는 불필요합니다.

-class TokenRepositoryImpl(): TokenRepository {
+class TokenRepositoryImpl : TokenRepository {

15-24: 에러 처리를 추가하세요.

DataStore 연산은 실패할 수 있지만 현재 구현에는 에러 처리가 없습니다. 디스크 I/O 오류, 권한 문제 등으로 인해 예외가 발생할 수 있습니다.

Result 타입을 반환하거나 try-catch로 예외를 처리하는 것을 고려하세요:

override suspend fun saveToken(context: Context, token: String) {
    try {
        context.datastore.edit {
            it[KEY] = token
        }
    } catch (e: Exception) {
        // 로깅 또는 에러 전파
    }
}

override suspend fun getToken(context: Context): String? {
    return try {
        val prefs = context.datastore.data.first()
        prefs[KEY]
    } catch (e: Exception) {
        null
    }
}
app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt (1)

3-3: 불필요한 import를 제거하세요.

android.R.attr.value import는 사용되지 않으며 API 서비스 인터페이스에 필요하지 않습니다.

-import android.R.attr.value
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt (2)

16-16: 초기 상태를 재검토하세요.

_uiState의 초기값이 Loading으로 설정되어 있습니다. 게시물 생성이 시작되지 않은 초기 상태에서 Loading을 표시하는 것은 사용자 경험을 해칠 수 있습니다. Idle이나 초기 상태를 나타내는 별도의 상태를 고려하세요.

-private val _uiState = MutableStateFlow<PostCreateUiState>(PostCreateUiState.Loading)
+private val _uiState = MutableStateFlow<PostCreateUiState>(PostCreateUiState.Idle) // Idle 상태가 있다면

29-32: 에러 로깅을 추가하는 것을 고려하세요.

에러 발생 시 UI 상태는 업데이트되지만 로깅이나 추가적인 에러 추적이 없습니다. 디버깅과 모니터링을 위해 로깅을 추가하는 것을 권장합니다.

 .onFailure { error ->
+    Log.e("PostCreateViewModel", "Failed to create post", error)
     _uiState.value = PostCreateUiState.Error(
         error.message ?: "error"
     )
 }
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt (1)

19-20: 삭제 상태의 초기값을 수정하세요.

_deleteUiStateLoading으로 초기화되어 있습니다. 삭제 작업이 시작되지 않은 상태에서 Loading을 표시하는 것은 부적절합니다. Idle 또는 초기 상태를 나타내는 상태값을 사용하세요.

-private val _deleteUiState = MutableStateFlow<DeletePostUiState>(DeletePostUiState.Loading)
+private val _deleteUiState = MutableStateFlow<DeletePostUiState>(DeletePostUiState.Idle) // Idle 상태가 있다면
app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (1)

99-101: 사용되지 않는 변수를 제거하세요.

route 변수가 선언되었지만 사용되지 않습니다. LoginScreen은 매개변수를 필요로 하지 않으므로 이 코드는 불필요합니다.

 composable<LoginRoute> { backStackEntry ->
-    val route = backStackEntry.toRoute<LoginRoute>()
-
     LoginScreen(
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt (2)

3-3: 불필요한 import를 제거하세요.

android.R.attr.password import는 사용되지 않습니다.

-import android.R.attr.password

20-21: 보안 고려사항: 비밀번호 관리

현재 비밀번호가 LoginUiState에 평문으로 저장되어 메모리 덤프 시 노출될 수 있습니다. 프로덕션 환경에서는 다음을 고려하세요:

  1. 비밀번호를 상태로 저장하지 않고 직접 매개변수로 전달
  2. 민감한 데이터 처리를 위한 별도의 보안 메커니즘 사용

학습 목적의 코드라면 현재 구조도 괜찮지만, 실제 서비스에서는 개선이 필요합니다.

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

19-36: 에러 메시지 일관성 개선 권장

Line 32의 기본 에러 메시지가 "error"로 되어 있어 getPostDetail의 "게시글 불러오기 실패"에 비해 구체성이 떨어집니다. 사용자에게 더 명확한 피드백을 제공하기 위해 "게시글 수정 실패"와 같은 설명적인 메시지로 변경하는 것을 고려해보세요.

                .onFailure { error ->
                    _uiState.value = PostEditUiState.Error(
-                        error.message ?: "error"
+                        error.message ?: "게시글 수정 실패"
                    )
                }
app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt (2)

11-23: 입력 유효성 검사 추가 권장

ID와 password에 대한 유효성 검사가 없습니다. 빈 문자열이나 부적절한 형식의 입력이 API로 전달될 수 있습니다. 네트워크 비용을 줄이고 사용자 경험을 개선하기 위해 기본적인 검증을 추가하는 것을 권장합니다.

예시:

override suspend fun signup(id: String, password: String): Result<LoginResponse> {
    if (id.isBlank() || password.isBlank()) {
        return Result.failure(Exception("아이디와 비밀번호를 입력해주세요"))
    }
    
    return runCatching {
        val response = apiService.signup(LoginRequest(username = id, password = password))
        // ... 기존 로직

25-39: 코드 중복 제거 고려

signuplogin 함수가 거의 동일한 구조를 가지고 있습니다. 공통 로직을 추출하여 중복을 줄일 수 있습니다.

private suspend fun authenticateUser(
    id: String,
    password: String,
    apiCall: suspend (LoginRequest) -> BaseResponse<LoginResponse>,
    errorMessage: String
): Result<LoginResponse> {
    return runCatching {
        val response = apiCall(LoginRequest(username = id, password = password))
        if (response.success && response.data != null) {
            response.data
        } else {
            throw Exception(response.message ?: errorMessage)
        }
    }.onFailure { error ->
        Log.e("LoginRepository", error.message.toString())
    }
}

override suspend fun signup(id: String, password: String): Result<LoginResponse> {
    return authenticateUser(id, password, apiService::signup, "회원가입 실패")
}

override suspend fun login(id: String, password: String): Result<LoginResponse> {
    return authenticateUser(id, password, apiService::login, "로그인 실패")
}
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt (1)

85-90: 토큰 노출 주의

Line 85에서 토큰을 UI에 직접 표시하고 있습니다. 실습 목적으로는 적절하지만, 실제 프로덕션 환경에서는 보안상 토큰을 UI에 노출하지 않아야 합니다. 추후 개발 시에는 이 부분을 제거하거나 디버그 모드에서만 표시되도록 수정하는 것을 권장합니다.

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

21-35: 에러 메시지 개선 권장

Line 31의 기본 에러 메시지가 "error"로 되어 있어 구체성이 떨어집니다. "게시글 목록을 불러올 수 없습니다"와 같은 설명적인 메시지로 변경하면 사용자 경험이 개선됩니다.

                .onFailure { error->
                    _postListUiState.value = PostListUiState.Error(
-                        error.message ?: "error"
+                        error.message ?: "게시글 목록을 불러올 수 없습니다"
                    )
                }
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt (1)

7-27: 인터페이스 설계가 일관성 있게 잘 구성되었습니다

모든 함수가 Result 타입을 반환하여 에러 처리가 일관적이며, suspend 함수로 비동기 처리를 명확히 했습니다.

다만 Line 25의 uploadImage 함수가 Map<String, String>을 반환하는 것은 타입 안정성이 떨어집니다. 이미지 업로드 응답을 위한 전용 데이터 클래스(예: ImageUploadResponse)를 정의하는 것을 고려해보세요.

예시:

data class ImageUploadResponse(val imageUrl: String)

interface PostRepository {
    suspend fun uploadImage(file: MultipartBody.Part): Result<ImageUploadResponse>
    // ... 나머지 함수들
}
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt (1)

45-50: Lifecycle-aware 상태 수집 권장

Line 50에서 collectAsState()를 사용하고 있습니다. collectAsStateWithLifecycle()을 사용하면 생명주기를 인식하여 백그라운드에서 불필요한 수집을 방지할 수 있습니다.

-    val uiState by viewModel.uiState.collectAsState()
+    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

필요한 import 추가:

import androidx.lifecycle.compose.collectAsStateWithLifecycle
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt (1)

12-88: 코드 중복 제거를 통한 유지보수성 향상 권장

모든 함수가 유사한 패턴을 따르고 있습니다: runCatching, 응답 검증, 데이터 반환/예외 발생, 에러 로깅. 공통 로직을 추출하여 중복을 줄이면 유지보수가 용이해집니다.

private suspend fun <T> executeApiCall(
    errorMessage: String,
    apiCall: suspend () -> BaseResponse<T>
): Result<T> {
    return runCatching {
        val response = apiCall()
        if (response.success && response.data != null) {
            response.data
        } else {
            throw Exception(response.message ?: errorMessage)
        }
    }.onFailure { error ->
        Log.e("PostRepository", error.message.toString())
    }
}

override suspend fun getPosts(): Result<List<PostResponse>> {
    return executeApiCall("게시글 불러오기 실패") { apiService.getPosts() }
}

override suspend fun createPost(author: String, request: PostCreateRequest): Result<PostResponse> {
    return executeApiCall("게시글 작성 실패") { apiService.createPost(author, request) }
}

override suspend fun getPostDetail(id: Long): Result<PostResponse> {
    return executeApiCall("게시글 불러오기 실패") { apiService.getPostDetail(id) }
}

// 나머지 함수들도 동일한 패턴으로 리팩토링

deletePost는 약간 다른 로직이므로 별도 헬퍼 함수나 조건 처리가 필요할 수 있습니다.

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

87-196: 로딩/에러 상태에 최소한의 UI 피드백을 추가하는 것이 좋겠습니다.

PostDetailUiState.LoadingPostDetailUiState.Error 분기에서 실제로 아무 것도 렌더링하지 않고 주석만 있어, 사용자가 “지금 로딩 중인지/에러인지”를 전혀 인지할 수 없습니다.

간단히라도 Box(modifier = Modifier.fillMaxSize().padding(paddingValues), contentAlignment = Alignment.Center) 안에 CircularProgressIndicator()"로딩 중", "불러오기 실패" 텍스트 정도를 추가해 두면 UX가 많이 개선될 것 같습니다.

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

56-98: 로딩/에러 UI의 레이아웃을 약간만 손보면 가독성이 좋아질 것 같습니다.

  • PostListUiState.Loading에서 CircularProgressIndicator()만 바로 배치하면, 상단 AppBar 아래 좌측 상단에 작게 떠서 사용자가 잘 인지하지 못할 수 있습니다.
  • PostListUiState.Error"로딩 실패" 텍스트도 paddingValues가 적용되지 않아 AppBar에 붙어 보일 가능성이 있습니다.

아래처럼 두 분기 모두 Box(Modifier.fillMaxSize().padding(paddingValues), contentAlignment = Alignment.Center) 안에서 표시하도록 정리하면, 로딩/에러 상태가 더 눈에 잘 들어오고 다른 상태와 레이아웃 일관성도 맞출 수 있을 것 같습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ea749c and 0da5c30.

📒 Files selected for processing (37)
  • app/build.gradle.kts (1 hunks)
  • app/src/main/AndroidManifest.xml (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/App.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/MainActivity.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt (2 hunks)
  • app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt (1 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/response/LoginResponse.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 (1 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/screen/LoginScreen.kt (1 hunks)
  • app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt (2 hunks)
  • app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt (10 hunks)
  • app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt (2 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/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)
  • gradle/libs.versions.toml (2 hunks)
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
🔇 Additional comments (22)
app/src/main/java/com/example/kuit6_android_api/ui/post/state/UploadImageUiState.kt (1)

5-15: UploadImageUiState 설계가 명확하고 일관적입니다.

이미지 업로드 흐름을 Idle / Loading / Success / Error로 나눈 sealed class 구조가 직관적이고, 다른 UI state 패턴과도 잘 맞아 보여서 사용성과 유지보수성이 좋습니다. 현재로서는 추가 수정이 필요 없어 보입니다.

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

10-20: LGTM!

ViewModel 팩토리 구현이 올바르며, DI 패턴을 적절히 따르고 있습니다. reified 타입 파라미터와 crossinline 사용이 정확합니다.

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

5-15: LGTM!

UI 상태 모델링이 올바르게 구현되었습니다. Success 상태에 의미 있는 페이로드(PostResponse)를 포함하고 있어 적절합니다.

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

17-17: LGTM!

커스텀 Application 클래스 등록이 올바르게 구현되었습니다. DI 컨테이너 초기화를 위한 표준 Android 패턴입니다.

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

5-9: LGTM!

로그인 요청 모델이 올바르게 구현되었습니다. @Serializable 어노테이션과 불변 프로퍼티 사용이 적절합니다.

gradle/libs.versions.toml (1)

33-33: DataStore 라이브러리 추가가 올바르게 구성되었습니다.

버전 카탈로그 항목이 올바르게 구조화되어 있습니다. 다만, alpha 버전(1.3.0-alpha01)을 사용하고 있으므로, 프로덕션 배포 전에 안정 버전으로 업그레이드하는 것을 고려하세요.

Also applies to: 73-73

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

5-10: LGTM! 깔끔한 응답 모델 구조입니다.

@serializable 어노테이션을 사용한 데이터 클래스 구조가 적절합니다. API 응답 모델로서 필요한 필드들이 명확하게 정의되어 있습니다.

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

6-12: LGTM! 표준적인 DI 컨테이너 초기화 패턴입니다.

Application 클래스에서 DI 컨테이너를 초기화하는 방식이 적절합니다. lateinit을 사용했지만, onCreate()에서 즉시 초기화되므로 안전합니다.

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

5-15: LGTM! 타입 안전한 UI 상태 관리 구조입니다.

sealed class를 사용하여 Loading, Success, Error 상태를 명확하게 구분한 것이 좋습니다. 이 패턴은 컴파일 타임에 모든 케이스를 처리하도록 강제하여 안전한 UI 상태 관리를 가능하게 합니다.

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

11-22: LGTM! 깔끔한 ViewModel 팩토리 구현입니다.

reified 타입 파라미터와 viewModelFactory DSL을 활용하여 타입 안전한 팩토리를 구현했습니다. LoginRepository와 TokenRepository 의존성을 주입하는 패턴이 적절합니다.

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

5-15: LGTM! 일관성 있는 UI 상태 패턴입니다.

PostEditUiState 및 PostListUiState와 동일한 sealed class 패턴을 사용하여 코드베이스 전체에 일관성을 유지하고 있습니다.

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

5-15: LGTM! 리스트 화면에 적합한 UI 상태 구조입니다.

다른 UI 상태 클래스들과 일관된 패턴을 유지하면서, Success 상태에서 List<PostResponse>를 포함하여 리스트 화면의 요구사항을 적절히 반영했습니다.

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

5-8: LGTM! 명확한 레포지토리 계약입니다.

suspend 함수와 Result<LoginResponse> 반환 타입을 사용하여 비동기 작업과 에러 처리를 타입 안전하게 정의했습니다. 인터페이스가 간결하고 명확합니다.

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

17-18: LGTM!

LoginRoute 추가가 올바르게 구현되었습니다. 모든 route 객체가 @Serializable로 표시되어 있어 타입 안전 네비게이션을 지원합니다.

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

5-8: LGTM!

TokenRepository 인터페이스가 간결하고 명확하게 정의되었습니다. suspend 함수를 사용하여 비동기 작업을 올바르게 지원합니다.

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

68-73: LGTM!

savedStateHandle을 사용하여 게시물 생성/수정 후 목록 새로고침을 트리거하는 패턴이 잘 구현되었습니다. 이는 화면 간 상태 동기화를 위한 좋은 접근 방식입니다.

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

38-50: LGTM!

게시글 상세 정보를 불러오는 로직이 올바르게 구현되었습니다. 에러 처리와 상태 관리가 적절합니다.

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

37-39: LGTM!

refresh 함수가 간결하고 명확하게 구현되었습니다. Pull-to-refresh 패턴에 적합합니다.

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

67-75: LGTM!

삭제 작업에 대한 처리가 적절합니다. Unit 반환 타입과 단순한 성공 여부 확인이 삭제 API의 특성에 맞게 구현되었습니다.

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

12-28: 검증 완료: 의존성 주입 구조가 정상적으로 작동 중입니다

TokenRepositoryImpl의 구현을 확인한 결과, Context 의존성이 다음과 같이 올바르게 처리되고 있습니다:

  • 생성자 레벨: TokenRepositoryImpl()는 인자 없이 안전하게 인스턴스화되며, AppContainer의 구현이 정확합니다.
  • 메서드 레벨: saveToken(context: Context)과 getToken(context: Context) 메서드에서 Context를 매개변수로 받아 runtime에 주입됩니다.
  • DataStore 접근: Context 확장 프로퍼티 context.datastore를 통해 DataStore에 접근하는 방식은 표준 Android 패턴입니다.

원래 제시된 우려사항은 확인 결과 문제가 아닙니다. 코드는 메서드 레벨 의존성 주입 패턴을 사용하고 있으며, 이는 Android에서 흔히 사용되는 유효한 방식입니다.

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

88-170: 상태 기반 UI 전환 구조는 전체적으로 잘 잡혀 있습니다.

PostEditUiState의 Loading/Success/Error 분기와, 성공 시에만 title/content를 한 번 초기화하는 패턴이 깔끔해서 이후 기능 추가(예: 저장 중 상태, 검증 에러 등)에도 확장성이 좋아 보입니다. UI 레이아웃도 paddingValuesverticalScroll 조합이 자연스럽습니다.

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

38-48: 전용 ViewModel + 초기 refresh() 트리거 흐름은 잘 정리되었습니다.

목록 화면에서 PostListViewModel을 외부에서 주입하고, LaunchedEffect(Unit)에서 한 번만 refresh()를 호출하는 구조가 명확하고 사이드 이펙트 위치도 잘 드러납니다. 다른 화면들과의 아키텍처 일관성도 좋아 보입니다.


// Coroutines
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.datastore.preferences)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

DataStore alpha 버전 사용을 확인하세요.

androidx.datastore.preferences 라이브러리의 alpha 버전(1.3.0-alpha01)을 사용하고 있습니다. 프로덕션 환경에서는 안정적인(stable) 버전 사용을 권장합니다.

다음 스크립트를 실행하여 최신 안정 버전을 확인하세요:


🏁 Script executed:

#!/bin/bash
# DataStore Preferences 최신 버전 확인
curl -s https://maven.google.com/web/index.html?q=androidx.datastore#androidx.datastore:datastore-preferences | grep -oP 'datastore-preferences/[\d.]+' | head -5

Length of output: 0


검증을 위해 추가 정보를 수집하겠습니다.

이제 실제 build.gradle.kts 파일과 버전 카탈로그를 확인하겠습니다.

Now awaiting script results to verify the actual version being used.


🏁 Script executed:

#!/bin/bash
# build.gradle.kts의 datastore 관련 라인 확인
echo "=== build.gradle.kts 라인 75-90 ===" 
sed -n '75,90p' app/build.gradle.kts

echo -e "\n=== libs.versions.toml 또는 gradle/libs.versions.toml 찾기 ==="
find . -name "libs.versions.toml" -o -name "gradle.properties" | head -5

Length of output: 213



---

🏁 Script executed:

```shell
#!/bin/bash
# build.gradle.kts 파일 위치 찾기
echo "=== build.gradle.kts 파일 위치 ==="
find . -name "build.gradle.kts" -type f | head -10

echo -e "\n=== libs.versions.toml 내용 ==="
cat ./git/gradle/libs.versions.toml

Length of output: 167



---

🏁 Script executed:

```shell
#!/bin/bash
# app/build.gradle.kts의 라인 81 주변 확인
echo "=== app/build.gradle.kts 라인 75-90 ==="
sed -n '75,90p' ./app/build.gradle.kts

echo -e "\n=== gradle/libs.versions.toml 내용 ==="
cat ./gradle/libs.versions.toml 2>/dev/null || echo "파일을 찾을 수 없습니다."

Length of output: 4394


이제 최신 안정 버전을 확인하겠습니다.


---

🌐 Web query:

```web
androidx.datastore preferences latest stable version 2024 2025

💡 Result:

  • Latest stable androidx.datastore:datastore-preferences (as of Nov 20, 2025): 1.1.7 (released May 20, 2025). [1]
  • Key 2024 stable releases: 1.1.0 (Apr 17, 2024) and 1.1.1 (May 1, 2024). [1][2]

Sources:
[1] Android Developers — DataStore release notes.
[2] Maven / mvnrepository listing for datastore-preferences.


androidx.datastore.preferences alpha 버전을 안정 버전으로 업그레이드하세요.

프로젝트에서 최신 안정 버전인 1.1.7(2025년 5월 20일 릴리스)로 업그레이드할 것을 권장합니다. gradle/libs.versions.toml의 datastorePreferences 버전을 다음과 같이 변경하세요:

datastorePreferences = "1.1.7"

변경 후 프로덕션 환경에 배포하기 전에 충분한 테스트를 수행해주세요.

🤖 Prompt for AI Agents
In app/build.gradle.kts around line 81, the project is using the alpha version
of androidx.datastore.preferences; update the version in
gradle/libs.versions.toml by setting datastorePreferences = "1.1.7" (the stable
2025-05-20 release) and then run a full build and regression tests to verify no
regressions before deploying to production.

Comment on lines +47 to +50
@POST("/api/upload")
suspend fun uploadImage(
@Part file: MultipartBody.Part
): BaseResponse<Map<String, String>>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

@multipart 어노테이션이 누락되었습니다.

uploadImage 함수는 @Part를 사용하여 MultipartBody.Part를 받고 있지만, 함수에 @Multipart 어노테이션이 없습니다. Retrofit에서 multipart 요청을 처리하려면 이 어노테이션이 필요합니다.

+@Multipart
 @POST("/api/upload")
 suspend fun uploadImage(
     @Part file: MultipartBody.Part
 ): BaseResponse<Map<String, String>>
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt around
lines 47 to 50, the suspend function uploadImage is missing the @Multipart
annotation even though it accepts a @Part MultipartBody.Part; add the @Multipart
annotation to the function declaration so Retrofit treats the request as
multipart/form-data and keep the existing @POST and @Part parameters unchanged.

Comment on lines 11 to 12
val Context.datastore: DataStore<Preferences> by preferencesDataStore("Token")
val KEY = stringPreferencesKey("token")
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

top-level 선언을 private으로 제한하세요.

Context.datastoreKEY가 파일의 top-level에서 public으로 선언되어 있습니다. 이는 네임스페이스 오염을 유발할 수 있으며, 다른 모듈이나 파일에서 실수로 접근할 수 있습니다.

다음 diff를 적용하여 접근 범위를 제한하세요:

-val Context.datastore: DataStore<Preferences> by preferencesDataStore("Token")
-val KEY = stringPreferencesKey("token")
+private val Context.datastore: DataStore<Preferences> by preferencesDataStore("token")
+private val KEY = stringPreferencesKey("token")

추가로 DataStore 이름도 "Token"에서 "token"으로 소문자로 통일하는 것을 권장합니다.

📝 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
val Context.datastore: DataStore<Preferences> by preferencesDataStore("Token")
val KEY = stringPreferencesKey("token")
private val Context.datastore: DataStore<Preferences> by preferencesDataStore("token")
private val KEY = stringPreferencesKey("token")
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt
around lines 11 to 12, the top-level declarations are public; make them
file-private by adding the private modifier to both the Context.datastore
extension and the KEY declaration, and change the DataStore name from "Token" to
lowercase "token" to standardize naming (i.e., private val Context.datastore:
DataStore<Preferences> by preferencesDataStore("token") and private val KEY =
stringPreferencesKey("token") so they are not exposed outside this file).

Comment on lines +25 to +32
@Composable
fun LoginScreen(
modifier: Modifier = Modifier,
onNavigateBack: ()-> Unit,
viewModel: LoginViewModel= viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

ViewModel 기본값 제거 권장

Line 29에서 viewModel: LoginViewModel = viewModel()로 기본값을 제공하고 있습니다. DI 컨테이너(AppContainer)를 사용하는 아키텍처에서는 ViewModel에 생성자 인자(LoginRepository, TokenRepository)가 필요하므로, 기본 viewModel() 호출은 실패할 수 있습니다. Navigation에서 ViewModelFactory를 통해 ViewModel을 주입받도록 기본값을 제거하는 것을 권장합니다.

fun LoginScreen(
    modifier: Modifier = Modifier,
    onNavigateBack: ()-> Unit,
-    viewModel: LoginViewModel= viewModel()
+    viewModel: LoginViewModel
    ) {
📝 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
@Composable
fun LoginScreen(
modifier: Modifier = Modifier,
onNavigateBack: ()-> Unit,
viewModel: LoginViewModel= viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
@Composable
fun LoginScreen(
modifier: Modifier = Modifier,
onNavigateBack: ()-> Unit,
viewModel: LoginViewModel
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current

Comment on lines +72 to +84
Row() {
val context = LocalContext.current
Button(onClick = {
viewModel.login(context)
}) {
Text("로그인")
}
Button(onClick = {
viewModel.signup(context)
}) {
Text("회원가입")
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

중복된 LocalContext 선언 제거

Line 73에서 LocalContext.current를 다시 선언하고 있지만, Line 32에서 이미 선언되어 사용 가능합니다. 중복 선언은 불필요하며 코드 가독성을 해칩니다.

            Row() {
-                val context = LocalContext.current
                Button(onClick = {

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/LoginScreen.kt
around lines 72 to 84, remove the redundant "val context = LocalContext.current"
declared inside the Row (line 73) and instead use the existing context variable
declared earlier (line 32); simply delete the inner declaration and pass the
outer context to viewModel.login(...) and viewModel.signup(...) so there is a
single context variable used throughout the composable.

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

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Success 상태에서 불필요한 Unit 프로퍼티를 제거하세요.

Success 상태에 Unit 타입의 프로퍼티를 포함하는 것은 의미가 없습니다. Unit은 "의미 있는 값이 없음"을 나타내므로, data class의 프로퍼티로 포함할 필요가 없습니다.

다음 diff를 적용하여 불필요한 프로퍼티를 제거하세요:

-    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 data class currently declares an unnecessary
property of type Unit; remove the Unit property and convert Success to either an
object or a parameterless data class (preferably an object) so it carries no
useless Unit field, updating any call sites that construct Success accordingly.

val id: String = "",
val password: String= "",
val isAutoLogin: Boolean= false,
val token: String = "",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

UI state에 token 필드가 필요한지 검토하세요.

token은 민감한 인증 정보로, TokenRepository를 통해 별도 관리되는 것으로 보입니다. LoginUiState에서 token 필드가 실제로 사용되는지 확인하고, 사용되지 않는다면 제거하는 것을 권장합니다.

다음 스크립트로 token 필드의 실제 사용처를 확인하세요:


🏁 Script executed:

#!/bin/bash
# LoginUiState.token 필드가 실제로 읽히거나 쓰이는지 확인

# token 필드에 접근하는 코드 검색
rg -nP --type=kt -C3 'LoginUiState.*\.token|\.copy\s*\([^)]*token\s*='

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Search for LoginUiState.token usage (retry without unsupported type)
echo "=== Searching for LoginUiState.token usage ==="
rg -nP --type=kotlin -C3 'LoginUiState.*\.token|\.copy\s*\([^)]*token\s*=' 2>/dev/null || rg -nP -C3 'LoginUiState.*\.token|\.copy\s*\([^)]*token\s*='

echo -e "\n=== Searching for token property assignment in LoginUiState ==="
rg -nP -C3 'token\s*[:=]' app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt

echo -e "\n=== Checking for LoginUiState instantiation and token usage ==="
rg -nP -C5 'LoginUiState\s*\(' --type=kotlin 2>/dev/null || rg -nP -C5 'LoginUiState\s*\('

Length of output: 3484


🏁 Script executed:

#!/bin/bash
# Search for where token is READ from LoginUiState
echo "=== Searching for token field being READ ==="
rg -nP -C3 'uiState.*\.token|state\.token|\.token\s*(,|\)|:|\s*\?|&&)' 

echo -e "\n=== Searching for LoginUiState usage in UI/Composable ==="
rg -nP -C3 'LoginUiState' --type=kotlin 2>/dev/null || rg -nP -C3 'LoginUiState'

echo -e "\n=== Check if token is used in any API calls or repositories ==="
rg -nP -C3 'token.*network|api.*token|send.*token|header.*token'

Length of output: 5345


token 필드를 LoginUiState에서 제거하세요.

검증 결과, token 필드가 실제로 사용되고 있음을 확인했습니다:

  • 쓰기: LoginViewModel.kt 라인 61에서 _uiState.update{it.copy(token = token ?: "")}로 업데이트
  • 읽기: LoginScreen.kt 라인 85에서 Text("토큰:${uiState.token} ")로 디버깅 목적 표시

그러나 원래 우려사항이 타당합니다. 민감한 인증 정보인 token을 UI state에 노출하는 것은 보안 위험입니다. TokenRepository를 통해 안전하게 관리되어야 하며, UI state에는 포함되면 안 됩니다.

필요한 수정사항:

  • LoginUiState.kt 라인 8: val token: String = "", 제거
  • LoginViewModel.kt 라인 61: _uiState.update{it.copy(token = token ?: "")} 제거
  • LoginScreen.kt 라인 85: Text("토큰:${uiState.token} ") 제거
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt
around line 8, remove the token property declaration (val token: String = "") so
the UI state no longer carries the sensitive token; in
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt
around line 61 remove the _uiState.update { it.copy(token = token ?: "") } call
and any other code that writes the token into UI state; in
app/src/main/java/com/example/kuit6_android_api/ui/post/view/LoginScreen.kt
around line 85 remove the debug Text("토큰:${uiState.token} ") that reads/displays
the token; ensure token handling stays inside TokenRepository and no other UI
state or view code exposes the token.

Comment on lines +37 to +46
fun signup(context: Context) {
viewModelScope.launch {
loginRepository.signup(
id = uiState.value.id,
password = uiState.value.password
).onSuccess {
tokenRepository.saveToken(context, it.token)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

로딩 상태와 에러 처리를 추가하세요.

signup 함수에 로딩 상태나 에러 처리가 없어서 사용자가 작업 진행 상황이나 실패 여부를 알 수 없습니다.

LoginUiStateisLoadingerror 필드를 추가하고 다음과 같이 구현하세요:

fun signup(context: Context) {
    viewModelScope.launch {
        _uiState.update { it.copy(isLoading = true, error = null) }
        loginRepository.signup(
            id = uiState.value.id,
            password = uiState.value.password
        ).onSuccess {
            tokenRepository.saveToken(context, it.token)
            _uiState.update { it.copy(isLoading = false) }
        }.onFailure { error ->
            _uiState.update { 
                it.copy(isLoading = false, error = error.message ?: "회원가입 실패") 
            }
        }
    }
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt
around lines 37 to 46, the signup function lacks loading and error handling; add
isLoading and error fields to LoginUiState, and in signup update the state
before the call to set isLoading=true and error=null, then on success save the
token and set isLoading=false, and on failure set isLoading=false and populate
error with the throwable message (or a default message) using _uiState.update {
... } in both success and failure branches so the UI can reflect progress and
errors.

Comment on lines 48 to 57
fun login(context: Context) {
viewModelScope.launch {
loginRepository.login(
id = uiState.value.id,
password = uiState.value.password
).onSuccess {
tokenRepository.saveToken(context, it.token)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

login 함수에도 동일한 개선이 필요합니다.

login 함수도 signup과 동일하게 로딩 상태와 에러 처리가 누락되었습니다. signup 함수에 적용한 개선 사항을 login 함수에도 동일하게 적용하세요.

🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt
around lines 48 to 57, the login function is missing the same loading state and
error handling added to signup; update login to set uiState loading=true before
calling loginRepository, handle the Result by onSuccess saving the token via
tokenRepository.saveToken(context, it.token) and clearing any error, handle
onFailure (or catch exception) to set uiState error with a user-friendly message
(or the exception message), and ensure loading is set back to false after
completion (use try/catch/finally or Result.onFailure + finally-equivalent) so
the UI reflects loading and error states consistently.

Comment on lines +12 to +16
class PostListViewModel (
private val postRepository : PostRepository
) : ViewModel() {
private val _PostList_uiState = MutableStateFlow<PostListUiState>(PostListUiState.Loading) // 변경 가능 상태
val postListUiState: StateFlow<PostListUiState> = _PostList_uiState.asStateFlow() // 읽기 전용
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

네이밍 컨벤션 수정 필요

Line 15의 _PostList_uiState가 PascalCase를 사용하고 있어 Kotlin 네이밍 컨벤션을 위반합니다. 프로퍼티명은 camelCase를 따라야 합니다.

-    private val _PostList_uiState = MutableStateFlow<PostListUiState>(PostListUiState.Loading)
-    val postListUiState: StateFlow<PostListUiState> = _PostList_uiState.asStateFlow()
+    private val _postListUiState = MutableStateFlow<PostListUiState>(PostListUiState.Loading)
+    val postListUiState: StateFlow<PostListUiState> = _postListUiState.asStateFlow()

그리고 Line 23, 27, 30에서도 _postListUiState로 변경해야 합니다.

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/viewmodel/PostListViewModel.kt
around lines 12 to 16, the private MutableStateFlow is named `_PostList_uiState`
which violates Kotlin camelCase conventions; rename it to `_postListUiState` and
update all usages accordingly (including the references on lines 23, 27 and 30)
so the private variable and its asStateFlow exposure use the new camelCase
identifier consistently.

@koongmai koongmai changed the title 8주차 실습 8주차 미션 / 안드로이드 1조 박경민 Nov 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants