-
Notifications
You must be signed in to change notification settings - Fork 10
9주차 미션 / 안드로이드 1조 정일혁 #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
게시글 생성, 수정, 삭제 시 사용자에게 피드백을 제공하기 위해 Snackbar를 추가했습니다. * **MainActivity**: `Scaffold`와 `SnackbarHost`를 설정하여 앱 전체에서 Snackbar를 사용할 수 있도록 UI 구조를 변경했습니다. * **NavGraph**: 각 화면(Screen)으로 `SnackbarHostState`를 전달하도록 수정했습니다. * **PostCreateScreen, PostEditScreen, PostDetailScreen**: 게시글 생성, 수정, 삭제 작업이 성공적으로 완료되면 Coroutine Scope 내에서 Snackbar를 호출하여 사용자에게 "게시글이 작성되었습니다.", "게시글이 수정되었습니다.", "게시글이 삭제되었습니다."와 같은 메시지를 표시하도록 구현했습니다.
기존 ViewModel에서 직접 API 통신을 하던 구조를 Repository 패턴을 적용하여 개선했습니다. 의존성 주입(DI)을 위해 Application 클래스와 AppContainer를 도입하고, ViewModelProvider.Factory를 사용하여 ViewModel에 Repository를 주입하도록 변경했습니다.
* **App, di, repository 패키지 추가**:
* `App.kt`: Application 클래스를 생성하여 DI 컨테이너(`AppContainer`)를 초기화합니다.
* `AppContainer.kt`: `PostRepository`의 인스턴스를 생성하고 관리합니다.
* `PostRepository.kt`, `PostRepositoryImpl.kt`: API 호출 로직을 ViewModel에서 분리하여 Repository 계층으로 이동했습니다.
* **ViewModel 리팩토링**:
* `PostListViewModel.kt`: 게시글 목록 조회를 담당하는 ViewModel을 새로 생성하고, `StateFlow`를 사용하여 UI 상태(`PostListUiState`)를 관리합니다.
* `PostViewModel.kt`: 게시글 목록 조회(`getPosts`) 관련 로직을 `PostListViewModel`으로 이전했습니다.
* `postViewModelFactory.kt`: `PostRepository`를 ViewModel에 주입하기 위한 `ViewModelProvider.Factory`를 구현했습니다.
* **UI 및 화면 로직 수정**:
* `PostListScreen.kt`: `PostListViewModel`과 `PostListUiState`를 사용하도록 변경하여, 로딩, 성공, 에러 상태에 따라 다른 UI를 표시하도록 구현했습니다.
* `NavGraph.kt`: `PostListScreen`에 `PostListViewModel`을 `viewModelFactory`를 통해 주입하도록 수정했습니다.
* **기타 변경 사항**:
* `AndroidManifest.xml`: `Application` 클래스를 `App`으로 지정했습니다.
* `UriUtils.kt`: `ui.post.viewmodel` 패키지에서 `util` 패키지로 파일을 이동했습니다.
기존의 단일 `PostViewModel`을 `PostCreateViewModel`, `PostDetailViewModel`, `PostEditViewModel`로 분리하여 각 화면의 역할을 명확히 했습니다. 또한, `PostRepository`를 도입하여 데이터 소스를 추상화하고 ViewModel과 데이터 계층 간의 의존성을 분리했습니다.
* **Repository 추가**:
* `PostRepository`: `ApiService`를 사용하여 게시글 CRUD 및 이미지 업로드 API를 호출하는 로직을 캡슐화했습니다.
* **ViewModel 분리**:
* `PostCreateViewModel`: 게시글 생성 및 이미지 업로드 기능을 담당합니다.
* `PostDetailViewModel`: 게시글 상세 조회 및 삭제 기능을 담당합니다.
* `PostEditViewModel`: 게시글 수정, 상세 조회 및 이미지 업로드 기능을 담당합니다.
* **의존성 주입(DI)**:
* `AppContainer`: `PostRepository`의 인스턴스를 생성하고 관리하여 ViewModel에 주입하는 역할을 합니다.
* 각 Screen(`PostCreateScreen`, `PostDetailScreen`, `PostEditScreen`)에서는 `AppContainer`를 통해 해당 화면에 맞는 ViewModel을 주입받도록 수정했습니다.
기존의 단일 `PostViewModel`을 `PostListViewModel`으로 이름을 변경하고 목록 조회 기능만 남도록 역할을 축소했습니다. 또한, 각 ViewModel에서 개별적으로 관리하던 상태 변수들을 `PostListUiState`, `PostCreateUiState`, `PostDetailUiState`, `PostEditUiState`와 같은 데이터 클래스로 통합하여 상태 관리를 개선했습니다.
* **ViewModel 분리 및 리팩토링**:
* `PostViewModel.kt`: 기존의 다기능 ViewModel을 삭제했습니다.
* `PostListViewModel.kt`: 게시글 목록 조회를 담당하는 ViewModel을 새로 생성하고, `PostListUiState`를 사용하여 UI 상태를 관리합니다. `getPosts()` 메서드는 `refresh()`로 이름을 변경했습니다.
* `PostCreateViewModel`, `PostDetailViewModel`, `PostEditViewModel`: 각 ViewModel의 상태 변수들(`uploadedImageUrl`, `isUploading`, `postDetail` 등)을 `UiState` 데이터 클래스로 캡슐화하여 관리하도록 수정했습니다.
* **UI 및 화면 로직 수정**:
* `PostListScreen.kt`: `PostListViewModel`과 `PostListUiState`를 사용하도록 변경했습니다.
* `PostCreateScreen.kt`, `PostDetailScreen.kt`, `PostEditScreen.kt`: 각 화면에서 ViewModel의 `UiState`를 참조하여 UI를 렌더링하도록 수정했습니다.
ViewModel을 주입하는 방식을 익명 객체 `ViewModelProvider.Factory`를 사용하도록 변경하여 코드를 간소화했습니다. 또한, `PostListScreen`에 `DisposableEffect`와 `LifecycleEventObserver`를 사용하여 화면이 다시 활성화될 때마다 게시글 목록을 자동으로 새로고침하는 기능을 추가했습니다.
* **ViewModel 팩토리 리팩토링**:
* `PostCreateScreen`, `PostDetailScreen`, `PostEditScreen`, `PostListScreen`: `viewModel()` 함수에 익명 클래스로 `ViewModelProvider.Factory`를 구현하여 ViewModel 인스턴스를 생성하도록 수정했습니다.
* **게시글 목록 자동 새로고침**:
* `PostListScreen.kt`: 화면의 생명주기(Lifecycle)가 `ON_RESUME` 상태일 때 `viewModel.refresh()`를 호출하도록 `DisposableEffect`를 추가했습니다. 이를 통해 다른 화면에서 게시글을 생성, 수정, 삭제하고 목록 화면으로 돌아왔을 때 변경 사항이 즉시 반영됩니다.
* **ViewModel 예외 처리 개선**:
* `PostCreateViewModel.kt`, `PostEditViewModel.kt`: 이미지 업로드 시 파일 변환에 실패하는 경우, `runCatching` 대신 `if (file == null)` 체크를 통해 명시적으로 예외를 처리하도록 수정했습니다.
`PostCreateScreen`에서 ViewModel을 생성하는 방식을 수정했습니다. * **PostCreateScreen.kt**: `ViewModelProvider.Factory`를 사용하여 `PostCreateViewModel`을 생성하도록 변경했습니다. 이를 통해 수동으로 생성한 `AppContainer`에서 Repository를 가져와 ViewModel에 주입할 수 있도록 구조를 개선했습니다.
기존의 단일 PostViewModel을 PostCreateViewModel, PostDetailViewModel, PostEditViewModel로 분리하여 각 화면의 역할을 명확히 했습니다. 또한, PostRepository를 도입하여 데이터 소스를 추상화하고 ViewModel과 데이터 계층 간의 의존성을 분리했습니다. * **Repository 추가**:PostRepository: ApiService를 사용하여 게시글 CRUD 및 이미지 업로드 API를 호출하는 로직을 캡슐화했습니다. * **ViewModel 분리**:PostCreateViewModel: 게시글 생성 및 이미지 업로드 기능을 담당합니다.PostDetailViewModel: 게시글 상세 조회 및 삭제 기능을 담당합니다.PostEditViewModel: 게시글 수정, 상세 조회 및 이미지 업로드 기능을 담당합니다. * **의존성 주입(DI)**:AppContainer: PostRepository의 인스턴스를 생성하고 관리하여 ViewModel에 주입하는 역할을 합니다.각 Screen(PostCreateScreen, PostDetailScreen, PostEditScreen)에서는 AppContainer를 통해 해당 화면에 맞는 ViewModel을 주입받도록 수정했습니다.
기존의 단일 `PostViewModel`을 `PostListViewModel`으로 이름을 변경하고 목록 조회 기능만 남도록 역할을 축소했습니다. 또한, 각 ViewModel에서 개별적으로 관리하던 상태 변수들을 `PostListUiState`, `PostCreateUiState`, `PostDetailUiState`, `PostEditUiState`와 같은 데이터 클래스로 통합하여 상태 관리를 개선했습니다.
* **ViewModel 분리 및 리팩토링**:
* `PostViewModel.kt`: 기존의 다기능 ViewModel을 삭제했습니다.
* `PostListViewModel.kt`: 게시글 목록 조회를 담당하는 ViewModel을 새로 생성하고, `PostListUiState`를 사용하여 UI 상태를 관리합니다. `getPosts()` 메서드는 `refresh()`로 이름을 변경했습니다.
* `PostCreateViewModel`, `PostDetailViewModel`, `PostEditViewModel`: 각 ViewModel의 상태 변수들(`uploadedImageUrl`, `isUploading`, `postDetail` 등)을 `UiState` 데이터 클래스로 캡슐화하여 관리하도록 수정했습니다.
* **UI 및 화면 로직 수정**:
* `PostListScreen.kt`: `PostListViewModel`과 `PostListUiState`를 사용하도록 변경했습니다.
* `PostCreateScreen.kt`, `PostDetailScreen.kt`, `PostEditScreen.kt`: 각 화면에서 ViewModel의 `UiState`를 참조하여 UI를 렌더링하도록 수정했습니다.
ViewModel을 주입하는 방식을 익명 객체 `ViewModelProvider.Factory`를 사용하도록 변경하여 코드를 간소화했습니다. 또한, `PostListScreen`에 `DisposableEffect`와 `LifecycleEventObserver`를 사용하여 화면이 다시 활성화될 때마다 게시글 목록을 자동으로 새로고침하는 기능을 추가했습니다.
* **ViewModel 팩토리 리팩토링**:
* `PostCreateScreen`, `PostDetailScreen`, `PostEditScreen`, `PostListScreen`: `viewModel()` 함수에 익명 클래스로 `ViewModelProvider.Factory`를 구현하여 ViewModel 인스턴스를 생성하도록 수정했습니다.
* **게시글 목록 자동 새로고침**:
* `PostListScreen.kt`: 화면의 생명주기(Lifecycle)가 `ON_RESUME` 상태일 때 `viewModel.refresh()`를 호출하도록 `DisposableEffect`를 추가했습니다. 이를 통해 다른 화면에서 게시글을 생성, 수정, 삭제하고 목록 화면으로 돌아왔을 때 변경 사항이 즉시 반영됩니다.
* **ViewModel 예외 처리 개선**:
* `PostCreateViewModel.kt`, `PostEditViewModel.kt`: 이미지 업로드 시 파일 변환에 실패하는 경우, `runCatching` 대신 `if (file == null)` 체크를 통해 명시적으로 예외를 처리하도록 수정했습니다.
`PostCreateScreen`에서 ViewModel을 생성하는 방식을 수정했습니다. * **PostCreateScreen.kt**: `ViewModelProvider.Factory`를 사용하여 `PostCreateViewModel`을 생성하도록 변경했습니다. 이를 통해 수동으로 생성한 `AppContainer`에서 Repository를 가져와 ViewModel에 주입할 수 있도록 구조를 개선했습니다.
Android Gradle Plugin(agp)의 버전을 8.13.0에서 8.13.1로 업데이트했습니다.
…IT6_Android_API into JeongIlhyuk/week7
각 화면(Screen)에서 `ViewModel`을 주입받는 부분에 `postViewModelFactory`를 통해 `Repository`가 수동으로 주입된다는 내용의 주석을 추가했습니다. * `PostCreateScreen.kt` * `PostDetailScreen.kt` * `PostEditScreen.kt` * `PostViewModelFactory.kt`
기존의 단일 `PostViewModel`을 기능별로 분리하여 각 화면의 역할을 명확히 했습니다. 또한, `PostRepository`를 도입하여 데이터 소스를 추상화하고 ViewModel과 데이터 계층 간의 의존성을 분리했습니다.
* **Repository 추가 및 수정**:
* `PostRepository`: 게시글 CRUD(생성, 상세 조회, 수정, 삭제) 및 이미지 업로드 기능을 위한 메서드를 추가했습니다.
* `PostRepositoryImpl`: 추가된 인터페이스 메서드들을 구현했습니다.
* **ViewModel 분리**:
* `PostViewModel`을 삭제하고, 기능에 따라 아래의 ViewModel들로 분리했습니다.
* `PostCreateViewModel`: 게시글 생성 및 이미지 업로드 기능을 담당합니다.
* `PostDetailViewModel`: 게시글 상세 조회 및 삭제 기능을 담당합니다.
* `PostEditViewModel`: 게시글 수정, 상세 조회 및 이미지 업로드 기능을 담당합니다.
* **의존성 주입(DI)**:
* `AppContainer`: `PostRepository`의 인스턴스를 생성하고 관리하여 ViewModel에 주입하는 역할을 합니다.
* `NavGraph`: 각 화면(`PostCreateScreen`, `PostDetailScreen`, `PostEditScreen`)에서 `AppContainer`를 통해 해당 화면에 맞는 ViewModel을 주입받도록 수정했습니다.
`PostListViewModel`에서 `StateFlow`를 사용하여 UI 상태를 관리하도록 변경하고, `PostListScreen`에서는 로딩, 성공, 에러 상태에 따라 다른 UI를 표시하도록 개선했습니다.
* **PostListViewModel.kt**:
* 기존 `mutableStateOf`로 관리하던 `uiState`를 `StateFlow`로 변경했습니다.
* 게시글 목록 조회 결과에 따라 `Loading`, `Success`, `Error` 상태를 갖는 `PostListUiState`를 `emit`하도록 수정했습니다.
* 게시글을 불러오는 `loadPosts()` 함수를 추가하고, `refresh()`에서 이를 호출하도록 변경했습니다.
* **PostListScreen.kt**:
* `collectAsState()`를 사용하여 `ViewModel`의 `uiState`를 구독하도록 변경했습니다.
* `when` 문을 사용하여 `PostListUiState`의 상태(Loading, Success, Error)에 따라 `CircularProgressIndicator` 또는 `LazyColumn`을 표시하도록 UI 로직을 수정했습니다.
* 기존 `DisposableEffect`를 사용한 생명주기 기반의 새로고침 로직을 제거하고, `LaunchedEffect`를 사용하여 화면 진입 시 데이터를 불러오도록 단순화했습니다.
DI 컨테이너인 `AppContainer`의 패키지 경로를 `di`에서 `data/di`로 변경하여 데이터 계층의 일부임을 명확히 했습니다. 또한, `lazy` 초기화를 사용하여 `AppContainer` 내에서 `ApiService`와 `PostRepository`의 인스턴스 생성을 최적화했습니다.
* **`AppContainer` 패키지 이동**:
* 기존 `di/AppContainer.kt` 파일을 `data/di/AppContainer.kt`로 이동했습니다.
* **`lazy` 초기화 적용**:
* `AppContainer` 내의 `apiService`와 `postRepository` 프로퍼티를 `lazy`를 사용하여 초기화하도록 수정했습니다. 이를 통해 해당 인스턴스들이 실제로 처음 사용될 때 생성되도록 변경했습니다.
* **`PostRepository` 인터페이스 확장**:
* `getPostDetail`, `createPost`, `deletePost`, `editPost` 함수를 추가하여 게시글의 상세 조회 및 CRUD 기능을 위한 메서드를 정의했습니다.
`PostListScreen`의 `LaunchedEffect` 블록에 Side Effect에 대한 설명을 주석으로 추가하여 코드의 가독성을 높였습니다.
`PostDetailViewModel`에서 `mutableStateOf`로 관리하던 UI 상태(`UiState`)를 `MutableStateFlow`로 변경하여 데이터 흐름을 개선했습니다.
* **PostDetailViewModel**:
* `uiState`의 타입을 `mutableStateOf`에서 `MutableStateFlow`로 변경했습니다.
* `uiState` 업데이트 로직을 `copy()` 대신 `update()`를 사용하도록 수정했습니다.
* **PostCreateViewModel**:
* 불필요한 주석을 제거했습니다.
* **AppContainer**:
* 의존성 주입에 대한 설명 주석을 추가했습니다.
기존에 `mutableStateOf`로 관리하던 ViewModel의 UI 상태(`uiState`)를 `StateFlow`를 사용하도록 리팩토링했습니다. 이를 통해 UI 상태를 보다 안전하고 효율적으로 관리할 수 있으며, Compose UI와의 데이터 흐름을 개선했습니다.
* **ViewModel 변경 사항 (`PostCreateViewModel`, `PostDetailViewModel`, `PostEditViewModel`)**:
* `var uiState by mutableStateOf(...)`를 private `MutableStateFlow`와 public `StateFlow`로 변경했습니다.
* 상태 업데이트 로직을 `_uiState.update { ... }`를 사용하도록 수정했습니다.
* **Screen 변경 사항 (`PostCreateScreen`, `PostDetailScreen`, `PostEditScreen`)**:
* ViewModel의 `StateFlow`를 `collectAsState()`로 구독하여 UI 상태를 관찰하도록 수정했습니다.
로그인 기능을 구현하기 위해 `LoginScreen`과 `LoginViewModel`을 추가했습니다. 또한, `PostListScreen`에 로그인 화면으로 이동하는 버튼을 추가했습니다.
* **LoginScreen.kt, LoginViewModel.kt 추가**:
* 로그인 UI를 구성하는 `LoginScreen` 컴포저블을 새로 생성했습니다.
* 향후 로그인 로직을 처리할 `LoginViewModel`을 추가했습니다.
* **PostListScreen.kt 수정**:
* 기존의 게시글 작성 FAB(FloatingActionButton) 외에, 로그인 화면으로 이동하기 위한 FAB을 추가했습니다.
* 두 개의 FAB을 화면 하단에 나란히 배치하기 위해 `FabPosition.Center`를 사용하고 `Row`로 감쌌습니다.
각 ViewModel의 UI 상태를 관리하기 위해 `sealed class`를 사용하여 `UiState`를 정의하고, 이를 통해 로딩, 성공, 에러 상태를 명확하게 구분하도록 리팩토링했습니다. 이로 인해 UI는 ViewModel에서 전달된 상태에 따라 적절한 화면(로딩 인디케이터, 성공 화면, 에러 메시지)을 표시할 수 있게 되었습니다.
* **`ui.post.state` 패키지 및 UiState 클래스 추가**:
* `PostCreateUiState`, `PostDetailUiState`, `PostEditUiState` `sealed class`를 추가하여 각 화면의 UI 상태(Loading, Success, Error)를 정의했습니다.
* **ViewModel 리팩토링**:
* `PostCreateViewModel`, `PostDetailViewModel`, `PostEditViewModel`: 기존에 `data class`로 관리하던 UI 상태를 새로 정의된 `sealed class` 기반의 `UiState`로 변경했습니다. API 호출 결과(성공/실패)에 따라 `StateFlow`가 `Loading`, `Success`, `Error` 상태를 가지도록 수정했습니다.
* **Screen 리팩토링**:
* `PostCreateScreen`, `PostDetailScreen`, `PostEditScreen`: `UiState`의 상태(Loading, Success, Error)에 따라 분기하여 각각 로딩 인디케이터, 컨텐츠, 에러 메시지를 표시하도록 `when` 표현식을 사용해 UI 로직을 수정했습니다.
로그인 및 회원가입 기능을 구현하고, 관련 화면과 데이터 계층을 추가했습니다.
* **로그인/회원가입 화면 추가**:
* `LoginScreen.kt`: 아이디, 비밀번호 입력 필드, 자동 로그인 체크박스, 로그인 및 회원가입 버튼으로 구성된 UI를 구현했습니다.
* `LoginViewModel.kt`: `LoginUiState`를 사용하여 ID, 비밀번호, 자동 로그인 상태를 관리합니다.
* **Repository 및 데이터 모델 추가**:
* `LoginRepository.kt`, `LoginRepositoryImpl.kt`: `ApiService`를 사용하여 서버에 로그인 및 회원가입을 요청하는 로직을 캡슐화했습니다.
* `LoginRequest.kt`, `LoginResponse.kt`: 로그인/회원가입 API 요청 및 응답에 사용될 데이터 클래스를 추가했습니다.
* **API 및 DI 설정**:
* `ApiService.kt`: `/login`, `/signup` 엔드포인트를 추가했습니다.
* `AppContainer.kt`: `LoginRepository`의 인스턴스를 생성하고 주입할 수 있도록 설정했습니다.
* **네비게이션 및 UI 수정**:
* `NavGraph.kt`: `PostListScreen`에서 `LoginScreen`으로 이동할 수 있도록 `LoginRoute`와 네비게이션 로직을 추가했습니다.
* `PostListScreen.kt`: 로그인 화면으로 이동하는 `Person` 아이콘의 플로팅 액션 버튼을 추가했습니다.
DataStore를 사용하여 로그인 성공 시 발급되는 토큰을 로컬에 저장하는 기능을 추가했습니다. 또한, Repository와 ViewModel을 구현하여 로그인 및 회원가입 API 통신 로직을 완성했습니다.
* **DataStore 및 TokenRepository 추가**:
* `TokenRepository` 및 `TokenRepositoryImpl`을 추가하여 DataStore를 통해 토큰을 저장하고 조회하는 로직을 캡슐화했습니다.
* `build.gradle.kts`와 `libs.versions.toml`에 `datastore-preferences` 의존성을 추가했습니다.
* **로그인/회원가입 기능 구현**:
* `LoginRepository` 및 `LoginRepositoryImpl`: `ApiService`를 사용하여 실제 로그인/회원가입 API를 호출합니다.
* `LoginViewModel`: `LoginRepository`를 주입받아 로그인/회원가입 요청을 처리하고, ViewModel 내에서 `viewModelScope`를 사용하여 코루틴으로 비동기 작업을 수행합니다.
* `LoginScreen`: 로그인, 회원가입 버튼 클릭 시 각각의 `ViewModel` 함수를 호출하도록 연결했습니다.
* **의존성 주입 (DI) 설정**:
* `loginViewModelFactory`: `LoginRepository`를 `LoginViewModel`에 주입하기 위한 `ViewModelProvider.Factory`를 구현했습니다.
* `AppContainer`: `TokenRepository` 인스턴스를 생성하고 관리하도록 추가했습니다.
* `NavGraph`: `LoginScreen`에 `loginViewModelFactory`를 통해 `LoginViewModel`을 주입하도록 수정했습니다.
* **API 및 데이터 모델 수정**:
* `ApiService`: `signup`, `login` 함수의 엔드포인트를 명시했습니다.
* `LoginResponse`: `@Serializable` 어노테이션을 추가하여 직렬화가 가능하도록 수정했습니다.
회원가입 또는 로그인 성공 시 서버로부터 받은 JWT를 `DataStore`에 저장하는 기능을 추가했습니다. 또한, 저장된 토큰을 조회하여 UI에 표시하는 기능을 구현했습니다.
* **Repository 추가 및 DI 수정**:
* `TokenRepository`: `DataStore`를 사용하여 토큰을 저장하고 조회하는 로직을 담당합니다.
* `AppContainer`: `TokenRepository`의 인스턴스를 생성하고 관리합니다.
* `LoginViewModelFactory`: `LoginViewModel`에 `TokenRepository`를 주입하도록 수정했습니다.
* **LoginViewModel**:
* 회원가입(`signup`) 및 로그인(`login`) 성공 시, `TokenRepository`를 통해 토큰을 저장합니다.
* `getToken` 함수를 추가하여 `DataStore`에 저장된 토큰을 조회하고 `LoginUiState`를 업데이트합니다.
* **LoginScreen**:
* 회원가입 및 로그인 버튼 클릭 시 `Context`를 `ViewModel`에 전달하도록 수정했습니다.
* "토큰 조회" 버튼과 토큰을 표시하는 `Text`를 추가하여 토큰 저장 및 조회 기능을 테스트할 수 있도록 UI를 변경했습니다.
자동 로그인 및 토큰 기반 인증을 위한 기반 코드를 추가했습니다. `AuthInterceptor`를 `Retrofit`에 추가하여 향후 모든 API 요청에 인증 토큰을 자동으로 포함하도록 설정하고, 자동 로그인 로직을 처리하기 위한 코드를 `LoginViewModel`에 추가했습니다.
* **API 계층**:
* `AuthInterceptor.kt`: API 요청을 가로채 인증 헤더를 추가하는 `AuthInterceptor` 클래스 파일을 생성했습니다. (구현은 아직 미완료)
* `RetrofitClient.kt`: 생성된 `AuthInterceptor`를 `OkHttpClient`에 추가하여 모든 API 요청에 적용되도록 설정했습니다.
* **UI 및 ViewModel**:
* `LoginScreen.kt`: 토큰 검증을 테스트하기 위한 "토큰 검증" 버튼 UI를 추가했습니다.
* `LoginViewModel.kt`: `init` 블록을 추가하여, 향후 자동 로그인 검증 로직을 구현할 위치를 마련했습니다.
`AuthInterceptor`를 `Retrofit`에 추가하여 모든 API 요청에 인증 토큰을 자동으로 포함하도록 설정하고, `DataStore`를 사용하여 자동 로그인 상태를 저장 및 조회하는 기능을 구현했습니다. 또한, 앱 시작 시 자동 로그인 상태를 확인하고 토큰을 검증하는 로직을 `LoginViewModel`에 추가했습니다.
* **API 계층**:
* `AuthInterceptor`: API 요청을 가로채 `Authorization` 헤더에 JWT 토큰을 자동으로 추가하는 인터셉터를 구현했습니다. `DataStore` 접근을 위해 `Context`를 받도록 수정했습니다.
* `RetrofitClient`: `AuthInterceptor`를 `OkHttpClient`에 추가하여 모든 API 요청에 적용되도록 설정했습니다. `Context` 주입을 위해 초기화(`init`) 메서드를 추가하고, 관련 객체들을 `lazy` 초기화로 변경했습니다.
* **Data 및 Repository 계층**:
* `TokenRepository`: 자동 로그인 여부(`isAutoLogin`)를 `DataStore`에 저장하고 조회하는 `saveAutoLogin`, `getAutoLogin` 함수를 추가했습니다.
* `LoginRepository`: `getPosts` API를 호출하여 토큰의 유효성을 검증하는 `verifyToken` 함수를 추가했습니다.
* **ViewModel 및 UI 상태**:
* `LoginViewModel`:
* `init` 블록에서 앱 시작 시 자동 로그인 상태를 `DataStore`에서 확인하고, 활성화된 경우 `verifyToken`을 호출하여 자동 로그인을 수행합니다.
* 자동 로그인 체크박스 상태 변경 시 `DataStore`에 즉시 저장하는 로직을 추가했습니다.
* 토큰 검증을 위한 `verifyToken` 함수와 관련 UI 상태(`isVerifying`, `verificationButtonText`)를 추가했습니다.
* `LoginUiState`: 토큰 검증 상태를 나타내는 `isVerifying`과 버튼 텍스트(`verificationButtonText`) 필드를 추가했습니다.
* **의존성 주입(DI) 및 기타**:
* `App`: `Application` 클래스에서 `RetrofitClient`를 `Context`로 초기화하도록 수정했습니다.
* `LoginViewModelFactory`: `LoginViewModel`에 `Application` 컨텍스트를 주입하도록 수정했습니다.
`LoginScreen`에 토큰 검증을 위한 로직을 추가하고, `ViewModel`의 UI 상태(`uiState`)와 연동하여 버튼의 텍스트를 동적으로 변경하도록 구현했습니다.
* **LoginScreen.kt 수정**:
* "토큰 검증" 버튼 클릭 시 `ViewModel`의 `verifyToken` 함수를 호출하도록 연결했습니다.
* `LaunchedEffect`를 사용하여 `uiState.verificationButtonText`의 변경을 감지하고, "토큰 검증" 버튼의 텍스트가 상태에 따라 (예: "토큰 검증", "토큰 검증 성공", "토큰 검증 실패") 동적으로 업데이트되도록 수정했습니다.
기존에 `getPosts` API를 사용하던 토큰 검증 로직을 새로운 `/api/auth/validate` 엔드포인트를 사용하도록 수정했습니다. 이 변경으로 인해 토큰 검증 로직이 더 명확해졌습니다.
* **ApiService.kt**:
* 토큰의 유효성을 검증하기 위한 `validateToken()` 함수와 `@GET("/api/auth/validate")` 엔드포인트를 새로 추가했습니다.
* **LoginRepositoryImpl.kt**:
* `verifyToken` 함수 내에서 `apiService.getPosts()`를 호출하던 부분을 새로 추가된 `apiService.validateToken()`으로 변경했습니다.
* 응답 성공 조건에 `response.data == true`를 추가하여 서버로부터의 검증 결과를 명시적으로 확인하도록 수정했습니다.
`AuthInterceptor` 클래스 내 코드의 가독성을 높이고 역할을 명확히 하기 위해 주석을 추가했습니다. * `Interceptor`의 역할과 요청을 가로채는 과정에 대한 설명을 추가했습니다. * `Authorization` 헤더를 추가하는 부분에 대한 설명을 보강하여, 모든 API 요청에 인증 토큰이 어떻게 포함되는지 명확히 했습니다.
`LoginViewModelFactory`에서 `Application` 컨텍스트를 사용하는 이유를 명확히 하기 위해 주석을 추가했습니다. * **LoginViewModelFactory.kt**: `Application` 컨텍스트를 전달하는 것이 더 안정적이라는 내용의 주석을 추가했습니다.
`PostListScreen.kt`에서 `PostListUiState.Success` 상태일 때 `(uiState as PostListUiState.Success).posts`를 `posts`라는 별도의 변수로 추출했습니다. 이로써 `LazyColumn`의 `items`에 전달되는 코드가 간결해지고 가독성이 개선되었습니다.
Hilt를 프로젝트에 적용하기 위해 필요한 Gradle 플러그인과 라이브러리 의존성을 추가했습니다. 이를 통해 향후 의존성 주입(DI)을 설정할 기반을 마련했습니다.
* **`build.gradle.kts` (Project & App 수준)**:
* 프로젝트 레벨 `build.gradle.kts`에 `ksp`와 `hilt` 플러그인을 추가했습니다.
* 앱 레벨 `build.gradle.kts`에 `com.google.dagger.hilt.android`와 `com.google.devtools.ksp` 플러그인을 적용했습니다.
* `hilt-android`, `hilt-navigation-compose` 등 Hilt 관련 라이브러리와 어노테이션 프로세싱을 위한 `ksp` 의존성을 추가했습니다.
* **`gradle/libs.versions.toml`**:
* `hilt`, `hiltNavigationCompose`, `ksp` 등의 라이브러리 버전과 의존성 목록을 추가하여 중앙에서 관리하도록 설정했습니다.
수동으로 관리하던 의존성 주입(DI) 방식을 Hilt로 전환하여 보일러플레이트 코드를 제거하고, 각 계층의 의존성을 자동으로 관리하도록 개선했습니다. 또한, 토큰 관련 로직을 `TokenRepository`로 이전하고, 불필요한 `Context` 전달을 제거하여 코드 구조를 단순화했습니다.
* **의존성 주입(DI) Hilt로 전환**:
* `@HiltAndroidApp`을 `Application` 클래스에, `@AndroidEntryPoint`를 `MainActivity`에 추가하여 Hilt 설정을 완료했습니다.
* `NetworkModule`과 `RepositoryModule`을 생성하여 `Retrofit`, `OkHttpClient`, `ApiService` 및 각종 `Repository`의 의존성을 Hilt가 관리하도록 설정했습니다.
* 기존의 수동 DI 컨테이너인 `AppContainer`를 삭제했습니다.
* `AuthInterceptor`, `PostRepositoryImpl`, `LoginRepositoryImpl`, `TokenRepositoryImpl`에 `@Inject` 생성자를 추가하여 Hilt를 통해 의존성을 주입받도록 수정했습니다.
* **토큰 및 인증 로직 리팩토링**:
* `TokenRepository`에서 `Context` 의존성을 제거하고, 생성자에서 `@ApplicationContext`를 주입받아 사용하도록 변경했습니다. 이로 인해 `saveToken`, `getToken`, `saveAutoLogin`, `getAutoLogin` 함수에서 `Context` 파라미터가 제거되었습니다.
* `LoginRepository`에 있던 토큰 검증(`verifyToken`) 로직을 `TokenRepository`의 `validateToken` 함수로 이동시켜 역할과 책임을 명확히 했습니다.
* `AuthInterceptor`가 `TokenRepository`를 직접 주입받아 사용하도록 변경하여, `Context`를 통해 `TokenRepositoryImpl` 인스턴스를 생성하던 방식을 개선했습니다.
* **ViewModel 및 Repository 수정**:
* `LoginViewModel`: `TokenRepository`의 변경된 함수 시그니처에 맞춰 `Context` 전달 코드를 제거하고, 토큰 검증 시 `tokenRepository.validateToken()`을 호출하도록 수정했습니다.
* `LoginRepository`: `verifyToken` 함수를 제거하고 `TokenRepository`로 책임을 위임했습니다.
* 각 `Repository` 구현체(`PostRepositoryImpl`, `LoginRepositoryImpl`, `TokenRepositoryImpl`)에 `@Singleton` 스코프를 적용했습니다.
토큰 관련 API 호출 로직을 `TokenRepository`에서 `TokenApiRepository`로 분리하여 역할과 책임을 명확히 했습니다. 또한, Hilt를 사용하여 `LoginViewModel`에 의존성을 주입하도록 구조를 변경했습니다.
* **Repository 분리**:
* `TokenApiRepository` 인터페이스와 `TokenApiRepositoryImpl` 구현체를 새로 생성하여, `ApiService`를 통한 실제 토큰 검증 API 호출 로직을 이전했습니다.
* 기존 `TokenRepository`에서는 토큰 검증과 관련된 API 호출 함수(`getValidateTokenApi`, `validateToken`)들을 제거하고, DataStore를 통한 로컬 토큰 관리 역할에만 집중하도록 수정했습니다.
* **Hilt 의존성 주입 적용**:
* `LoginViewModel`을 `@HiltViewModel`로 전환하고, 생성자 주입(`@Inject`)을 통해 `LoginRepository`, `TokenRepository`, `TokenApiRepository`를 받도록 수정했습니다. 이로 인해 더 이상 `ViewModelFactory`가 필요하지 않게 되었습니다.
* `RepositoryModule`: Hilt가 `TokenApiRepository`의 구현체를 주입할 수 있도록 바인딩 설정을 추가했습니다.
* **ViewModel 수정**:
* `LoginViewModel` 내에서 토큰 검증을 위해 `tokenRepository.validateToken()`를 호출하던 부분을 새로운 `tokenApiRepository.validateToken()` 호출로 변경했습니다.
* `init` 블록에서 `Application` 컨텍스트를 직접 참조하던 부분을 제거하고, `verifyToken` 함수 호출 시 `Context`를 받도록 수정하여 의존성을 낮췄습니다.
서버 API를 통해 토큰의 유효성을 검증하고, 검증 실패 시 저장된 토큰을 삭제하고 자동 로그인 상태를 해제하는 로직을 추가했습니다.
* **Repository 계층**:
* `TokenRepository` 및 `TokenRepositoryImpl`: `validateToken` 함수를 추가하여 서버에 토큰 유효성 검증을 요청하는 기능을 구현했습니다. 검증 실패 시 예외를 발생시키도록 수정되었습니다.
* `TokenRepositoryImpl`의 `validateToken`은 API 응답 `response.data`가 `true`일 경우에만 성공으로 간주합니다.
* **ViewModel 계층 (`LoginViewModel.kt`)**:
* 기존 `verifyToken` 함수의 이름을 `validateToken`으로 변경했습니다.
* `validateToken` 함수 내에서 토큰 검증 실패 시, `TokenRepository`를 호출하여 기기에 저장된 토큰을 삭제(`deleteToken`)하고 자동 로그인 설정을 해제(`saveAutoLogin(false)`)하는 로직을 추가했습니다.
* `init` 블록에서 `Context` 사용이 불가능하여 앱 시작 시 `validateToken`을 자동 호출하던 로직을 제거했습니다.
* **UI 계층 (`LoginScreen.kt`)**:
* "토큰 검증" 버튼 클릭 시 `validateToken` 함수가 호출되도록 수정했습니다.
DataStore를 이용한 로컬 토큰 관리와 API를 통한 토큰 검증 로직의 책임을 분리하여 코드 구조를 개선했습니다.
`TokenRepository`는 DataStore를 사용한 토큰 저장, 조회, 삭제 등 로컬 데이터 처리에만 집중하도록 수정하고, API 통신을 통한 토큰 유효성 검증 기능은 `TokenApiRepository`로 이전했습니다.
* **Repository 분리**:
* `TokenApiRepository`: 토큰 유효성을 검증하는 `validateToken` API 호출 로직을 담당하는 새로운 Repository 인터페이스와 구현체(`TokenApiRepositoryImpl`)를 추가했습니다.
* `TokenRepository`: 기존의 `validateToken` 함수를 제거하여, DataStore를 통한 로컬 토큰 관리 책임만 갖도록 역할을 명확히 했습니다.
* **ViewModel 수정**:
* `LoginViewModel`: 토큰 검증 시, 기존의 `TokenRepository` 대신 새로운 `TokenApiRepository`의 `validateToken` 함수를 호출하도록 수정했습니다.
기존의 수동 의존성 주입(DI) 방식인 `AppContainer`와 `ViewModelFactory`를 제거하고, Hilt를 적용하여 앱 전반의 의존성 주입을 자동화했습니다. 이를 통해 보일러플레이트 코드를 줄이고, 보다 간결하고 확장성 있는 구조로 개선했습니다.
* **의존성 주입(DI) 전환**:
* `@HiltAndroidApp` 어노테이션을 `Application` 클래스에 추가했습니다.
* `AppContainer`와 관련된 모든 코드를 삭제하고, Hilt가 의존성 컨테이너 역할을 하도록 변경했습니다.
* `Retrofit`, `OkHttpClient`, `ApiService` 등 네트워크 관련 객체를 제공하는 `NetworkModule`을 추가했습니다.
* `Repository` 구현체들을 Hilt에 바인딩하는 `RepositoryModule`을 추가했습니다.
* **`ViewModel` 및 화면(Screen) 수정**:
* 모든 `ViewModel`에 `@HiltViewModel` 어노테이션을 추가하고, 생성자에 `@Inject`를 사용하여 의존성을 주입받도록 수정했습니다.
* `PostListScreen`, `PostDetailScreen`, `LoginScreen` 등 모든 화면에서 `viewModel()` 팩토리 함수 대신 `hiltViewModel()`을 사용하여 `ViewModel` 인스턴스를 가져오도록 변경했습니다.
* **`ViewModelFactory` 삭제**:
* 수동으로 `ViewModel`을 생성하던 `LoginViewModelFactory.kt`와 `PostViewModelFactory.kt` 파일을 삭제했습니다.
* **`RetrofitClient` 싱글톤 객체 삭제**:
* Hilt의 `NetworkModule`에서 `Retrofit` 관련 객체들의 생명주기를 관리하도록 변경함에 따라, 기존의 `RetrofitClient` 싱글톤 객체를 삭제했습니다.
`DataStore`를 사용하는 로컬 토큰 관리 로직과 API를 통해 토큰을 검증하는 로직을 분리하기 위해 `RepositoryModule`의 의존성 주입 설정을 수정했습니다.
* **`RepositoryModule.kt` 수정**:
* 기존에 `TokenRepository`를 바인딩하던 `bindTokenApiRepository` 함수의 이름을 `bindTokenRepository`로 변경하여 명확성을 높였습니다.
* `TokenApiRepository`와 그 구현체인 `TokenApiRepositoryImpl`에 대한 새로운 바인딩(`bindTokenApiRepository`)을 추가했습니다.
* 이에 따라 반환 타입을 `TokenRepository`에서 `TokenApiRepository`로 수정하고, 구현체를 `TokenApiRepositoryImpl`로 변경했습니다.
기존에 수동으로 생성하고 주입하던 객체들을 Hilt를 사용하여 자동으로 관리하도록 전환했습니다. 이로 인해 코드의 보일러플레이트가 감소하고 모듈성이 향상되었습니다.
* **NetworkModule (`NetworkModule.kt`):**
* `OkHttpClient`, `Retrofit`, `ApiService`, `HttpLoggingInterceptor` 등의 네트워크 관련 객체들을 `Provides` 어노테이션을 사용하여 Hilt 모듈로 정의했습니다.
* `AuthInterceptor`가 `OkHttpClient`에 자동으로 주입되도록 설정했습니다.
* **Repository 계층 (`LoginRepositoryImpl.kt`, `TokenRepositoryImpl.kt`):**
* `@Inject` 생성자 주입을 사용하여 `ApiService`나 `Context` 같은 의존성을 Hilt로부터 자동으로 받도록 수정했습니다.
* `@Singleton` 어노테이션을 추가하여 앱 전체에서 단일 인스턴스로 관리되도록 설정했습니다.
* **API 계층 (`AuthInterceptor.kt`):**
* `TokenRepository`를 `@Inject`를 통해 생성자에서 주입받도록 수정했습니다.
* **ViewModel 계층 (`LoginViewModel.kt`):**
* `@HiltViewModel` 어노테이션을 추가하고, `@Inject`를 사용한 생성자 주입을 통해 `LoginRepository`, `TokenRepository`, `TokenApiRepository`를 자동으로 주입받도록 변경했습니다.
`AuthInterceptor`의 생성자에서 Hilt가 의존성을 주입하는 방식에 대한 설명을 명확하게 수정했습니다.
* **AuthInterceptor.kt**:
* `TokenRepository` 주입 관련 주석을 "Hilt가 TokenRepositoryImpl을 생성해서 주입"으로 변경하여, Hilt가 구현체를 주입하는 과정을 더 명확히 설명하도록 수정했습니다.
기존의 ViewModel 팩토리를 통한 수동 의존성 주입 방식에서 Hilt를 사용한 방식으로 전환했습니다. 각 ViewModel에 `@HiltViewModel` 어노테이션을 추가하고, 생성자에 `@Inject`를 사용하여 Repository 의존성을 자동으로 주입받도록 수정했습니다. * `PostListViewModel` * `PostEditViewModel` * `PostDetailViewModel` * `PostCreateViewModel`
|
Warning
|
| Cohort / File(s) | 변경 요약 |
|---|---|
의존성 주입 프레임워크 설정 app/build.gradle.kts, build.gradle.kts, gradle/libs.versions.toml, app/src/main/AndroidManifest.xml, app/src/main/java/com/example/kuit6_android_api/App.kt |
Hilt, KSP 플러그인 추가; DataStore, Hilt 라이브러리 및 컴파일러 의존성 추가; App 클래스에 @HiltAndroidApp 주석 추가 및 전역 인스턴스 참조 구현 |
네트워크 계층 리팩토링 app/src/main/java/com/example/kuit6_android_api/data/di/NetworkModule.kt, app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt, app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt |
RetrofitClient 싱글톤 제거; Hilt NetworkModule에서 HttpLoggingInterceptor, OkHttpClient, Json, Retrofit, ApiService 제공자 추가; signup, login, validateToken 엔드포인트 추가 |
인증 및 토큰 관리 app/src/main/java/com/example/kuit6_android_api/data/api/AuthInterceptor.kt, app/src/main/java/com/example/kuit6_android_api/data/model/request/LoginRequest.kt, app/src/main/java/com/example/kuit6_android_api/data/model/response/LoginResponse.kt, app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepository.kt, app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt, app/src/main/java/com/example/kuit6_android_api/data/repository/TokenApiRepository.kt, app/src/main/java/com/example/kuit6_android_api/data/repository/TokenApiRepositoryImpl.kt |
AuthInterceptor로 Authorization 헤더 추가; LoginRequest/LoginResponse 모델 정의; DataStore 기반 TokenRepository 구현; 토큰 검증용 TokenApiRepository 추가 |
저장소 계층 app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepository.kt, app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt, app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt, app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt, app/src/main/java/com/example/kuit6_android_api/data/di/RepositoryModule.kt |
LoginRepository 인터페이스 및 구현 추가 (signup/login); PostRepository 확장 (CRUD 및 이미지 업로드); Hilt RepositoryModule에서 모든 저장소 바인딩 |
UI 상태 클래스 app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt |
각 화면의 상태를 모델링하는 sealed class 및 data class 추가 (Loading, Success, Error 상태) |
ViewModel 리팩토링 app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt |
단일 PostViewModel 제거; 화면별 독립 ViewModel 추가 (각각 StateFlow 기반 상태 관리 및 useCase 메서드 구현) |
화면 구성 업데이트 app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt, app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt |
LoginScreen 새로 추가; 모든 화면에 Hilt ViewModel 주입, SnackbarHostState 파라미터, 상태 기반 UI 렌더링 적용 |
네비게이션 및 메인 액티비티 app/src/main/java/com/example/kuit6_android_api/ui/navigation/Routes.kt, app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt, app/src/main/java/com/example/kuit6_android_api/MainActivity.kt |
LoginRoute 추가; NavGraph에 snackBarState 파라미터 및 로그인 네비게이션 추가; MainActivity에 @AndroidEntryPoint, Scaffold 기반 snackbar 호스트 추가 |
유틸 및 설정 app/src/main/java/com/example/kuit6_android_api/util/UriUtils.kt, .idea/markdown.xml, .idea/deploymentTargetSelector.xml, .vscode/settings.json |
UriUtils를 ui.post.viewmodel에서 util 패키지로 이동; IDE 설정 파일 업데이트 |
Sequence Diagram(s)
sequenceDiagram
participant User as 사용자
participant LoginScreen
participant LoginViewModel
participant LoginRepository
participant TokenRepository
participant ApiService
participant AuthInterceptor
participant TokenStore as DataStore<br/>(Token)
User->>LoginScreen: 로그인 버튼 클릭
LoginScreen->>LoginViewModel: login(context)
LoginViewModel->>LoginRepository: login(id, password)
LoginRepository->>ApiService: login(LoginRequest)
ApiService->>AuthInterceptor: Authorization 헤더 검사
Note over AuthInterceptor: 토큰이 없거나 비어있음<br/>(첫 로그인)
AuthInterceptor->>ApiService: 요청 진행
ApiService-->>LoginRepository: BaseResponse<LoginResponse>
LoginRepository->>TokenRepository: saveToken(token)
TokenRepository->>TokenStore: token 저장
LoginRepository-->>LoginViewModel: Result.success(LoginResponse)
LoginViewModel->>LoginViewModel: uiState 업데이트 (token 표시)
LoginViewModel-->>LoginScreen: StateFlow 업데이트
LoginScreen-->>User: 토큰 표시
sequenceDiagram
participant PostListScreen
participant PostListViewModel
participant PostRepository
participant ApiService
participant AuthInterceptor
participant TokenRepository
PostListScreen->>PostListViewModel: refresh()
Note over PostListViewModel: uiState = Loading
PostListViewModel->>PostRepository: getPosts()
PostRepository->>ApiService: getPosts()
ApiService->>AuthInterceptor: 요청 인터셉트
AuthInterceptor->>TokenRepository: getToken()
TokenRepository-->>AuthInterceptor: token 반환
AuthInterceptor->>AuthInterceptor: Authorization: Bearer {token}<br/>헤더 추가
AuthInterceptor->>ApiService: 수정된 요청 진행
ApiService-->>PostRepository: BaseResponse<List<PostResponse>>
PostRepository-->>PostListViewModel: Result.success(posts)
Note over PostListViewModel: uiState = Success(posts)
PostListViewModel-->>PostListScreen: StateFlow 업데이트
PostListScreen->>PostListScreen: LazyColumn으로<br/>게시글 목록 렌더링
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
특별히 주의가 필요한 영역:
- AuthInterceptor.kt: runBlocking 사용으로 메인 스레드 블로킹 가능성; TokenRepository 호출 효율성 검토 필요
- LoginRepositoryImpl.kt, PostRepositoryImpl.kt: 서버 응답 검증 로직 (response.success, response.data 체크)에서 예외 처리 패턴 일관성 확인
- 모든 ViewModel 클래스: StateFlow 초기화, coroutine 스코프 관리, 에러 처리 로직 검증
- PostCreateScreen.kt, PostEditScreen.kt, PostDetailScreen.kt: 상태 기반 UI 렌더링의 정확성 (Loading, Success, Error 상태 전환)
- NavGraph.kt: 네비게이션 라우팅 및 ViewModel 인스턴스 생성 흐름 검증
- NetworkModule.kt: Hilt 모듈의 의존성 주입 순서 및 싱글톤 스코프 적용 확인
Possibly related PRs
- 6주차 미션 #16: ApiService 및 RetrofitClient 싱글톤 관련 변경사항; 이 PR은 RetrofitClient를 제거하고 Hilt NetworkModule로 대체하는 반대 방향의 리팩토링
- 6주차 미션 / 안드로이드 2조 권태우 #18: ApiService 확장 및 Retrofit 클라이언트 설정에 대한 변경사항; 동일한 네트워킹 계층 파일을 수정하지만 상반된 접근 방식 적용
- 6주차 미션 / 안드로이드 1조 정일혁 #17: MainActivity Scaffold/Snackbar, NavGraph 시그니처, app/build.gradle.kts 등 동일한 진입점 및 네비게이션 코드 수정
Poem
🐰 의존성 주입으로 깔끔하게,
상태는 흐르고 뷰모델 춤춘다네.
토큰 담아 TokenStore에 보관하고,
AuthInterceptor 지킴이 되어
로그인 화면 반갑게 맞이한다! 🔐✨
Pre-merge checks and finishing touches
❌ Failed checks (1 warning, 1 inconclusive)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 9.59% which is insufficient. The required threshold is 80.00%. | You can run @coderabbitai generate docstrings to improve docstring coverage. |
|
| Title check | ❓ Inconclusive | PR 제목이 매우 광범위하고 구체적이지 않습니다. '9주차 미션 / 안드로이드 1조 정일혁'은 가장 핵심적인 변경사항을 설명하지 못하고 있습니다. | 제목을 변경하여 주요 변경사항을 명확히 표현해 주세요. 예: 'Hilt DI, DataStore, 인증 및 상태 관리 기능 구현' 또는 'Hilt 의존성 주입 및 로그인 기능 추가' |
✅ 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.
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
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-18: IDE 설정 파일은 .gitignore에 추가하는 것을 권장합니다.
.idea/디렉토리의 파일들은 개발자 환경별로 다르므로 버전 관리에 포함하지 않는 것이 일반적입니다. 특히 배포 타겟 경로는 개발자마다 다를 수 있어 충돌을 일으킬 수 있습니다.
.gitignore에 다음을 추가하는 것을 고려해보세요:.idea/deploymentTargetSelector.xmlapp/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (1)
70-84: Kotlin 인자 전달 규칙 위반 - 컴파일 오류 발생검증 결과, 리뷰 코멘트의 지적이 정확합니다.
PostEditScreen함수의 파라미터 순서는 다음과 같습니다:
postId: LongonNavigateBack: () -> UnitonPostUpdated: () -> UnitviewModel: PostEditViewModel = hiltViewModel()snackBarState: SnackbarHostStateNavGraph.kt의 호출부에서 처음 세 개는 named argument로 전달하고,
hiltViewModel()은 positional argument로 전달한 뒤snackBarState를 다시 named argument로 전달하고 있습니다. Kotlin 규칙상 named argument 이후에는 모든 argument가 named여야 하므로 컴파일 오류가 발생합니다.제안된 수정이 올바릅니다:
PostEditScreen( postId = route.postId, onNavigateBack = { navController.popBackStack() }, onPostUpdated = { navController.popBackStack() }, - hiltViewModel(), + viewModel = hiltViewModel(), snackBarState = snackBarState )
🧹 Nitpick comments (29)
app/src/main/java/com/example/kuit6_android_api/App.kt (1)
18-21: 전역 Application 인스턴스 패턴이 구현되었습니다.Companion object를 통한 전역 인스턴스 패턴이 구현되었습니다. 다만 Hilt를 사용하는 경우, 대부분의 컨텍스트 접근은 DI를 통해 이루어지므로 이 패턴의 필요성을 재검토하는 것을 권장합니다.
Hilt를 사용하면
@ApplicationContext어노테이션을 통해 컨텍스트를 주입받을 수 있습니다:@Inject constructor(@ApplicationContext private val context: Context)이 방식이 더 테스트 가능하고 DI 원칙에 부합합니다.
app/build.gradle.kts (1)
99-105: Hilt 버전 업데이트 권장 (선택사항)검증 결과, Hilt 2.50 버전에는 알려진 보안 취약점이 없습니다. 다만 최신 버전은 2.57.2이므로, 버그 수정 및 기능 개선을 위해 최신 버전으로 업데이트하는 것을 권장합니다.
의존성을 다음과 같이 업데이트하세요:
libs.hilt.android: 2.50 → 2.57.2libs.hilt.core: 2.50 → 2.57.2libs.hilt.navigation.compose: 해당 버전에 맞게 업데이트app/src/main/java/com/example/kuit6_android_api/data/repository/TokenApiRepository.kt (1)
3-7: Repository 인터페이스에서Context를 직접 받는 구조 재검토 제안
TokenApiRepository가Context를 파라미터로 받으면서 Android 프레임워크에 직접 의존하고 있습니다.
DataStore/Hilt를 이미 사용 중이라면,Context대신 토큰 저장소나 DataStore 인스턴스를 DI로 주입하고, 인터페이스는suspend fun validateToken(): Result<Boolean>처럼 플랫폼 독립적인 형태로 유지하는 편이 레이어 분리가 더 깔끔할 것 같습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.kt (1)
5-15: PostDetail 화면 상태 모델링이 깔끔합니다
Loading / Success / Error로 분리한 sealed class 설계가 명확해서 ViewModel·UI 양쪽에서 다루기 좋아 보입니다.
추가로 초기 진입 상태가 필요하다면object Idle : PostDetailUiState()같은 상태를 두는 것도 선택지이지만, 현재 요구사항 기준으로는 그대로도 충분해 보입니다.app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt (1)
5-17: 성공 상태에 업로드 진행 정보까지 함께 들어가 있어 약간 복합적인 상태입니다
Success안에postDetail,uploadedImageUrl,isUploading가 모두 모여 있어서, 시간이 지나 UI 로직이 더 복잡해지면 상태 의미를 추적하기 다소 어려워질 수 있습니다.예를 들어 다음처럼 상태를 더 세분화하는 것도 고려할 수 있습니다.
Success(postDetail: PostResponse, imageUrl: String?)Uploading(postDetail: PostResponse, localImageUri: Uri?)(또는isUploading을 별도 상태로 분리)현재 구현도 동작에는 문제가 없어 보이므로, 구조 개선은 추후 리팩터링 타이밍에 봐도 될 것 같습니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt (1)
66-69: ViewModel 주입 방식 주석과 실제 구현이 어긋납니다현재는
PostEditViewModel = hiltViewModel()로 Hilt를 통해 자동 주입하고 있는데, 바로 위 주석은 “postViewModelFactory를 통해 수동 주입(App Container)”라고 되어 있어 예전 구조 설명으로 보입니다.혼동을 줄이기 위해 해당 주석을 제거하거나, “Hilt를 통해 주입됩니다” 식으로 최신 구조에 맞게 업데이트하는 것을 추천합니다.
app/src/main/java/com/example/kuit6_android_api/data/model/request/LoginRequest.kt (1)
6-9: 코드 스타일: 콜론 뒤 공백 추가 권장Kotlin 코딩 컨벤션에 따라 타입 선언 시 콜론 뒤에 공백을 추가하는 것이 권장됩니다.
다음 diff를 적용하여 코드 스타일을 개선할 수 있습니다:
data class LoginRequest( - val username:String, - val password:String + val username: String, + val password: String )app/src/main/java/com/example/kuit6_android_api/data/model/response/LoginResponse.kt (1)
6-10: 코드 스타일: 콜론 뒤 공백 추가 권장Kotlin 코딩 컨벤션에 따라 타입 선언 시 콜론 뒤에 공백을 추가하는 것이 권장됩니다.
다음 diff를 적용하여 코드 스타일을 개선할 수 있습니다:
data class LoginResponse( - val token:String, - val userId:Long, - val username:String + val token: String, + val userId: Long, + val username: String )app/src/main/java/com/example/kuit6_android_api/data/api/AuthInterceptor.kt (2)
3-3: 미사용 import 제거 필요
Contextimport가 사용되지 않습니다.다음 import를 제거하세요:
-import android.content.Context
5-5: 미사용 import 제거 필요
TokenRepositoryImplimport가 사용되지 않습니다. Hilt가TokenRepository인터페이스를 통해 구현체를 주입하므로 직접 참조할 필요가 없습니다.다음 import를 제거하세요:
-import com.example.kuit6_android_api.data.repository.TokenRepositoryImplapp/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepository.kt (2)
4-4: 미사용 import 제거 필요
PostResponseimport가 사용되지 않습니다.다음 import를 제거하세요:
-import com.example.kuit6_android_api.data.model.response.PostResponse
6-9: 코드 스타일: 콜론 뒤 공백 추가 권장Kotlin 코딩 컨벤션에 따라 파라미터 타입 선언 시 콜론 뒤에 공백을 추가하는 것이 권장됩니다.
다음 diff를 적용하여 코드 스타일을 개선할 수 있습니다:
interface LoginRepository { - suspend fun signup(id:String,password:String):Result<LoginResponse> - suspend fun login(id:String,password:String):Result<LoginResponse> + suspend fun signup(id: String, password: String): Result<LoginResponse> + suspend fun login(id: String, password: String): Result<LoginResponse> }app/src/main/java/com/example/kuit6_android_api/MainActivity.kt (1)
11-66: Hilt 적용 및 Scaffold+Snackbar 도입 구조 좋음 (innerPadding 활용 여지)
@AndroidEntryPoint로MainActivity에 Hilt를 붙이고,SnackbarHostState를remember해NavGraph로 넘기는 구조가 명확하고 확장하기 좋습니다.Scaffold의 content lambda에서innerPadding파라미터를 현재 사용하지 않는데, 추후 Snackbar/시스템 바에 의해 내용이 가려지는 이슈가 생기면NavGraph나 상위 레이아웃에innerPadding을 전달해 적용하는 것도 고려해 볼 수 있겠습니다.지금 상태로도 동작에는 문제 없어 보이며, 제안은 선택사항입니다.
app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (1)
85-93: LoginRoute composable에서 사용되지 않는 route 제거 제안
composable<LoginRoute> { backStackEntry ->블록 안에서val route = backStackEntry.toRoute<LoginRoute>()를 선언하지만 실제로 사용하지 않고 있습니다. 파라미터와 변수를 모두 제거하면 코드가 조금 더 간결해집니다.- composable<LoginRoute> { backStackEntry -> - val route = backStackEntry.toRoute<LoginRoute>() - - LoginScreen( + composable<LoginRoute> { + LoginScreen( onNavigateBack = { navController.popBackStack() }, viewModel = hiltViewModel() ) }app/src/main/java/com/example/kuit6_android_api/data/repository/TokenApiRepositoryImpl.kt (1)
6-6: Repository 계층에서의 Context 의존성 및 미사용 파라미터 정리 제안
validateToken에서Context를 받지만 실제로 사용하지 않고, data/repository 계층이android.content.Context에 직접 의존하게 됩니다.
가능하다면TokenApiRepository/TokenApiRepositoryImpl시그니처에서Context를 제거하고, Context가 정말 필요한 처리는 ViewModel이나 별도의 DataStore/로컬 저장소 레이어에서 담당하도록 분리하는 것을 고려해 주세요.
현재 구현이 동작에는 문제 없으므로 점진적으로 정리해도 됩니다.Also applies to: 12-19
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt (1)
21-43:deletePost실패 시 에러 처리 로직 보완 권장
deletePost에서onSuccess만 처리하고,onFailure는 완전히 무시되고 있습니다. 삭제 실패 시에도 아무 피드백이 없어 사용자가 원인을 알기 어렵습니다.예를 들어 다음과 같이 개선을 고려해 볼 수 있습니다.
PostDetailUiState에 삭제 관련 에러 상태를 추가해서 화면에서 에러 메시지를 표시하거나onSuccess와 유사하게onError: (Throwable) -> Unit콜백을 받아 상위에서 토스트/스낵바 등을 보여주도록 위임현재 코드도 컴파일/런타임 문제는 없지만, UX 관점에서 한 번 정리해 보시는 것을 추천드립니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt (1)
28-33: 사용되지 않는 파라미터와 불필요한 로컬 상태 정리 제안
modifier,onNavigateBack파라미터를 선언만 하고 실제로 사용하지 않고 있습니다.
- 정말 필요 없다면 시그니처에서 제거하고,
- 필요하다면 상단 AppBar의 뒤로가기 버튼 등으로 실제 네비게이션에 연결해 두는 편이 좋습니다.
토큰 검증 버튼의
buttonText는remember + LaunchedEffect로 별도 상태를 유지하기보다,Text(uiState.verificationButtonText)처럼uiState값을 직접 사용하는 쪽이 더 단순하고 상태 기반 아키텍처와도 잘 맞습니다.크리티컬한 문제는 아니지만, 한 번 정리해 두면 코드 가독성과 유지보수성이 올라갈 것 같습니다.
Also applies to: 96-111
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt (1)
7-24: PostRepository 계약이 일관적이며 사용하기 좋습니다
Result<>로 감싼 CRUD 및 이미지 업로드 시그니처가 일관되어 있어, 호출 측에서 공통적으로onSuccess / onFailure패턴으로 처리하기 좋아 보입니다.향후 규모가 커지면
createPost/updatePost의 다수 파라미터를PostCreateRequest나 도메인 모델로 묶는 것도 고려해 볼 수 있지만, 현재 단계에서는 지금처럼 명시적인 시그니처도 충분히 이해하기 쉽습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt (2)
71-76: 상태 기반 UI 구조는 좋지만, 폼 입력값을 ViewModel 상태로 올리는 것도 고려해보세요
when (uiState)로Loading / Success / Error를 나눈 구조와, 성공 상태에서만 본문 폼을 보여주는 방식은 상태 기반 아키텍처 목표와 잘 맞습니다.다만 작성 화면의 핵심 입력값(
author,title,content,selectedImageUri)을 모두remember로컬 상태로만 관리하고 있어,
- 프로세스 재생성 / 화면 회전 등에서 값이 쉽게 초기화될 수 있고
PostCreateUiState와 폼 상태가 분리되어 일관성이 조금 떨어질 수 있습니다.가능하다면 이 값들을
PostCreateUiState.Success안으로 포함시키거나, ViewModel에서 별도의 상태로 관리하도록 올려서 화면은 그 상태만 구독하도록 만드는 것도 한 번 고려해 보시면 좋겠습니다.Also applies to: 120-325
41-41: Lifecycle-aware 수집과 업로드/작성 실패 피드백 보완 제안
viewModel.uiState.collectAsState()는 생명주기 연동이 없어, 화면이 백스택에 있어도 Flow 수집이 계속될 수 있습니다. 이미 다른 화면에서 사용 중인collectAsStateWithLifecycle로 통일하면 더 안전합니다.- 이미지 업로드의
onError블록과 게시글 작성 실패 시 처리(예:PostCreateUiState.Error로 전환 + 스낵바) 부분이 비어 있어, 실제 실패 시 사용자 피드백이 부족할 수 있습니다.
snackBarState.showSnackbar(error.message ?: "업로드에 실패했습니다")같은 형태로 간단히라도 에러를 노출하는 것을 추천드립니다.라이브러리 버전에 따라 사용 가능한 API가 다를 수 있으므로, 사용하는 compose/lifecycle 버전 문서를 한 번 확인해 보시면 좋겠습니다.
Also applies to: 71-71, 79-93, 288-300
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt (1)
74-115:uploadImage에서 메인 스레드 블로킹·에러 콜백 누락 가능성이 있습니다.
UriUtils.uriToFile, 멀티파트 생성,repository.uploadImage까지 모두 기본 디스패처(메인)에서 돌고 있어서 이미지 파일이 크거나 네트워크가 느리면 UI 가 끊길 수 있습니다.withContext(Dispatchers.IO)등을 사용해 파일 변환과 업로드만 IO 디스패처로 분리하는 것을 권장합니다.- 네트워크 업로드 실패(
onFailure)에서는onError콜백이 호출되지 않아, 호출 측이 콜백 기반으로만 결과를 처리한다면 실패를 감지하지 못합니다. 아래처럼onError를 함께 호출하는 쪽이 더 일관된 인터페이스입니다.- repository.uploadImage(body) - .onSuccess { imageUrl -> + repository.uploadImage(body) + .onSuccess { imageUrl -> @@ - .onFailure { error -> - _uiState.value = PostEditUiState.Error( - message = error.message ?: "업로드 실패" - ) - } + .onFailure { error -> + val message = error.message ?: "업로드 실패" + _uiState.value = PostEditUiState.Error(message = message) + onError(message) + }app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt (2)
40-80:Context파라미터가 사용되지 않아 ViewModel API 가 불필요하게 복잡합니다.
onAutoLoginChanged,signup,login,getToken모두context: Context를 인자로 받지만 내부에서 사용하지 않습니다. ViewModel 쪽에서는 가능하면 플랫폼 의존성을 줄이는 게 좋으니, 사용 계획이 없다면 시그니처에서context를 제거하고, 호출부(예:LoginScreen)도 같이 정리하는 것을 권장합니다.
51-71: 회원가입/로그인 실패 시 사용자 피드백이 없는 점을 보완하는 것이 좋겠습니다.현재
signup/login은onSuccess에서 토큰만 저장하고,onFailure에는 별도 처리 없이 결과를 무시합니다. 서버 오류나 인증 실패 시 버튼 상태, 에러 메시지(LoginUiState에 필드 추가 등), 스낵바 노출 중 한 가지 방식으로라도 실패를 UI 에 반영해 주면 디버깅과 UX 모두 좋아질 것 같습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt (1)
60-98: 이미지 업로드에서도 메인 스레드 블로킹·에러 콜백 일관성을 한 번 더 점검해 보시면 좋겠습니다.
UriUtils.uriToFile, 멀티파트 생성,repository.uploadImage까지 모두 기본 디스패처에서 실행되고 있어, 큰 이미지 업로드 시 UI 프레임 드롭이 발생할 수 있습니다. 파일 변환·업로드만Dispatchers.IO로 옮기는 것을 고려해 주세요.- 업로드 실패(
onFailure)에서는onError콜백이 호출되지 않아, 호출 측에서 콜백으로만 성공/실패를 구분하기 어렵습니다. 아래처럼 실패 시에도onError를 함께 호출하면PostEditViewModel.uploadImage와도 일관된 인터페이스가 됩니다.- repository.uploadImage(body) - .onSuccess { imageUrl -> + repository.uploadImage(body) + .onSuccess { imageUrl -> @@ - .onFailure { error -> - _uiState.value = PostCreateUiState.Error( - message = error.message ?: "업로드 실패" - ) - } + .onFailure { error -> + val message = error.message ?: "업로드 실패" + _uiState.value = PostCreateUiState.Error(message = message) + onError(message) + }app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt (1)
95-210:when분기 안에서의 캐스팅을 줄이면 가독성이 조금 더 좋아질 수 있습니다.현재는
when (uiState)분기 안에서uiState as PostDetailUiState.Success/Error로 다시 캐스팅해서 사용하고 있는데, 아래처럼when (val state = uiState)패턴을 쓰면 스마트 캐스트를 활용할 수 있어 중복 캐스팅이 줄어듭니다.when (val state = uiState) { is PostDetailUiState.Loading -> { /* ... */ } is PostDetailUiState.Success -> { val postDetail = state.postDetail // ... } is PostDetailUiState.Error -> { Text(text = state.message, /* ... */) } }필수 변경 사항은 아니지만, 이후 상태가 늘어났을 때 유지보수가 조금 더 쉬워집니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt (2)
35-49: 상태 기반 구조와 Hilt 연동은 좋지만, 수집 방식/초기 로딩 위치는 한 번 점검해 보면 좋겠습니다.
PostListViewModel = hiltViewModel()로 DI 연동하고,uiState를collectAsState()로 받아when으로 분기하는 구조는 깔끔합니다.- 다만 화면 라이프사이클을 고려하면
collectAsStateWithLifecycle()(도입되어 있다면)을 사용하는 쪽이 더 안전하고,- 초기 데이터 로딩을
LaunchedEffect(Unit)의refresh()에서 할지,ViewModel의init블록에서 할지 한 곳으로 모아두면 재구성/재진입 시 중복 요청 가능성을 줄일 수 있습니다.
57-75: 두 개의 FAB 배치 방식이 의도한 레이아웃인지 한 번 확인이 필요합니다.
floatingActionButton슬롯 안에서Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween)를 사용하면 로그인/작성 FAB이 화면 양 끝으로 벌어집니다. 동시에floatingActionButtonPosition = FabPosition.Center를 쓰고 있어, 속성 이름과 실제 배치가 다르게 느껴질 수 있습니다.
현재처럼 양 끝 배치가 의도라면 그대로 두셔도 되지만, 두 버튼을 중앙 근처에 두고 싶다면Arrangement.spacedBy나BottomAppBar/NavigationBar기반의 액션 배치도 고려해 볼 수 있습니다.app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt (1)
10-48: runCatching 기반 구현은 좋지만, 로그에서 Throwable 전체를 남기는 편이 디버깅에 더 유리합니다.
Result로 래핑하는 구조와 실패 시 서버 메시지를 포함한Exception을 던지는 패턴은 깔끔합니다. 다만 현재onFailure { Log.e("LoginRepository", error.message.toString()) }는 스택 트레이스를 남기지 않고, 메시지가 null이면"null"만 찍히는 문제가 있습니다. 아래처럼 Throwable을 함께 넘겨 두면 나중에 원인 분석이 훨씬 쉽습니다.- }.onFailure { error -> - Log.e("LoginRepository", error.message.toString()) - } + }.onFailure { error -> + Log.e("LoginRepository", "signup 실패", error) + } @@ - }.onFailure { error -> - Log.e("LoginRepository", error.message.toString()) - } + }.onFailure { error -> + Log.e("LoginRepository", "login 실패", error) + }app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt (1)
12-100: Repository 구현 패턴은 일관적이지만, 에러 로깅과 응답 처리 공통화를 고려해 볼 수 있습니다.
- 모든 메서드에서
runCatching으로 API 경계를 감싸는 구조는 명확하고 좋습니다.- 다만
getPosts에서만.onFailure { Log.e("PostRepository", ...) }로 로그를 남기고,getPostDetail/createPost/updatePost/deletePost/uploadImage는 예외를Result로만 전달하고 있어서, 실제 운영 환경에서 어떤 API가 실패했는지 로그만 보고 한눈에 파악하기 어렵습니다. 각 메서드에 동일한.onFailure { Log.e("PostRepository", it.message ?: "unknown error", it) }를 붙이거나, 이를 감싸는 private 헬퍼 함수로 공통화하면 가독성과 디버깅 모두 좋아질 것 같습니다.BaseResponse<T>의success/data != null체크와 실패 시Exception(response.message ?: "...")를 던지는 코드가 여러 메서드에서 반복되고 있으므로,BaseResponse<T>.getOrThrow(defaultMessage: String): T같은 확장 함수로 추출하는 것도 유지보수 관점에서 유리해 보입니다.uploadImage는BaseResponse<Map<String, String>>에서"imageUrl"키를 직접 꺼내고 있는데, 서버 계약이 고정이라면 문제는 없지만,ImageUploadResponse(imageUrl: String)같은 DTO를 하나 두고BaseResponse<ImageUploadResponse>형태로 받으면 타입 안전성과 IDE 자동완성이 더 좋아질 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
.kotlin/errors/errors-1762482068299.logis excluded by!**/*.log.kotlin/errors/errors-1763117299345.logis excluded by!**/*.log
📒 Files selected for processing (43)
.idea/deploymentTargetSelector.xml(1 hunks).idea/markdown.xml(1 hunks).vscode/settings.json(1 hunks)app/build.gradle.kts(3 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(4 hunks)app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt(2 hunks)app/src/main/java/com/example/kuit6_android_api/data/api/AuthInterceptor.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt(0 hunks)app/src/main/java/com/example/kuit6_android_api/data/di/NetworkModule.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/di/RepositoryModule.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/TokenApiRepository.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/repository/TokenApiRepositoryImpl.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(6 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(4 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(5 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/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/viewmodel/LoginViewModel.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/util/UriUtils.kt(2 hunks)build.gradle.kts(1 hunks)gradle/libs.versions.toml(3 hunks)
💤 Files with no reviewable changes (2)
- app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
- app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt
🧰 Additional context used
🧬 Code graph analysis (4)
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt (1)
app/src/main/java/com/example/kuit6_android_api/ui/post/component/PostItem.kt (1)
PostItem(36-149)
app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (1)
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt (1)
LoginScreen(28-114)
app/src/main/java/com/example/kuit6_android_api/MainActivity.kt (1)
app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (1)
NavGraph(16-96)
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt (1)
app/src/main/java/com/example/kuit6_android_api/util/DateTimeUtil.kt (1)
formatDateTime(3-16)
🔇 Additional comments (34)
.idea/markdown.xml (1)
1-8:.idea/디렉토리 파일의 버전 관리 방식 확인 필요이 파일은 IntelliJ IDEA IDE 설정 파일입니다. 일반적으로
.idea/디렉토리의 개인 설정 파일들은 버전 관리 대상이 아니며, 프로젝트의.gitignore에 포함되어야 합니다.파일 자체의 내용(Markdown Compose 미리보기 활성화)은 올바르게 구성되어 있지만, 다음을 확인해 주세요:
- 프로젝트의
.gitignore파일에서.idea/디렉토리가 제외되어 있는지 확인- IDE 설정 파일을 의도적으로 커밋하는 경우, 프로젝트 컨벤션에 따라 문서화되어 있는지 확인
.vscode/settings.json (1)
1-3: VS Code Java 설정이 적절하게 추가되었습니다.Java 프로젝트의 자동 빌드 설정 동기화를 활성화하는 설정이 개발자 경험을 개선합니다. Hilt 마이그레이션과 같은 주요 아키텍처 변경 작업 중에 빌드 설정이 자동으로 유지되도록 하는 좋은 관례입니다.
build.gradle.kts (1)
5-6: LGTM! Hilt 및 KSP 플러그인 설정이 올바릅니다.최상위 build.gradle에서
apply false로 플러그인을 선언하는 것은 올바른 접근입니다. 하위 모듈에서 필요에 따라 적용할 수 있습니다.app/build.gradle.kts (2)
8-9: LGTM! Hilt 및 KSP 플러그인이 올바르게 적용되었습니다.모듈 레벨에서 Hilt와 KSP 플러그인을 적용하는 것이 정확합니다.
84-85: DataStore 의존성 추가가 올바릅니다.토큰 관리를 위한 DataStore Preferences 의존성이 적절하게 추가되었습니다.
gradle/libs.versions.toml (6)
10-13: Hilt 및 KSP 버전 정의가 적절합니다.Hilt 2.50과 KSP 2.0.21-1.0.28 버전이 추가되었습니다. KSP 버전이 Kotlin 버전(2.0.21)과 일치하는 것이 올바릅니다.
37-38: DataStore 버전이 추가되었습니다.DataStore Preferences 1.1.7 버전이 적절하게 정의되었습니다.
47-52: Hilt 라이브러리 정의가 올바릅니다.Hilt 관련 라이브러리들이 적절하게 정의되었습니다. 다만 line 52의
hilt-manager는 실제로androidx.hilt:hilt-compiler를 참조하고 있는데, 이것이 의도된 것인지 확인이 필요합니다.
hilt-manager항목이androidx.hilt:hilt-compiler를 참조하는 것이 맞는지 확인해주세요. 일반적으로hilt-compiler는 이미 다른 항목으로 정의되어 있으므로 중복될 수 있습니다.
85-86: DataStore 라이브러리 정의가 올바릅니다.DataStore Preferences 라이브러리가 적절하게 추가되었습니다.
93-94: Hilt 및 KSP 플러그인 정의가 올바릅니다.플러그인 정의가 적절한 형식으로 추가되었습니다.
2-2: AGP 8.13.1 업데이트 확인 완료Android Gradle Plugin 8.13.1은 패치 업데이트(2025년 11월 10일 발행)로 버그 수정과 소규모 API 변경을 포함합니다. 이 버전에서는 DependencyVariantSelection API가 deprecated되고 LocalDependencySelection으로 대체됩니다. 코드베이스에서 이 deprecated API의 사용을 확인했으나 해당 API 사용이 없으므로 업데이트는 안전합니다.
app/src/main/AndroidManifest.xml (1)
17-17: LGTM! Hilt Application 클래스가 올바르게 등록되었습니다.
android:name=".App"속성을 통해 Hilt 기반 Application 클래스를 등록하는 것이 정확합니다. 이를 통해 앱 시작 시 Hilt DI가 초기화됩니다.app/src/main/java/com/example/kuit6_android_api/App.kt (1)
8-16: LGTM! Hilt Application 클래스가 올바르게 구현되었습니다.
@HiltAndroidApp어노테이션과 Application 클래스 확장이 정확합니다. 전역 인스턴스 패턴도 적절하게 구현되었습니다.app/src/main/java/com/example/kuit6_android_api/util/UriUtils.kt (1)
1-41: Uri 관련 유틸을util패키지로 분리한 방향이 적절합니다
UriUtils를 ViewModel 패키지에서 공용util패키지로 옮긴 덕분에, 여러 ViewModel에서 재사용하기가 더 자연스러워졌고 레이어 책임도 더 명확해진 것 같습니다.
uriToFile/getFileName구현도use를 사용해 스트림·커서 자원 정리가 잘 되어 있어 현재 형태로 충분히 안정적으로 보입니다.app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt (1)
79-96: 업로드 실패 시나리오 검증 완료 — 제안된 개선사항은 유효하나 실패 시나리오 설명은 수정 필요실제 코드 동작을 확인한 결과:
현재 동작:
uploadImage실패 시PostEditUiState.Error로 상태 전환 (Success 상태 유지 X)- Error 상태에서는 에러 메시지 화면 표시, 수정 버튼 UI 렌더링 안 됨
- 따라서
selectedImageUri != null이면서uploadedImageUrl == null인 상태에서 버튼 클릭 불가능실제 문제:
imagePickerLauncher의onSuccess,onError콜백이 빈 상태 (주석만 있음) → 사용자에게 피드백 없음- 업로드 진행 중/완료 여부를 UI로 알 수 없음
개선 방안 유효성:
제안하신 수정은 여전히 권장됩니다:- val imageUrl = if (selectedImageUri != null) { - successState.uploadedImageUrl - } else { - postDetail.imageUrl - } + val imageUrl = successState.uploadedImageUrl ?: postDetail.imageUrl추가로:
onError콜백에서snackBarState.showSnackbar("이미지 업로드에 실패했습니다")등으로 사용자 피드백 제공- 필요시 실패 시
selectedImageUri = null처리app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt (1)
5-15: LGTM! 깔끔한 상태 관리 패턴 구현Sealed class를 사용한 UI 상태 표현이 적절합니다:
Loading상태를data object로 선언하여 싱글톤으로 관리Success와Error상태에 필요한 데이터를 포함- Sealed class를 통해 컴파일 타임에 모든 케이스를 처리할 수 있어 안전함
app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt (1)
3-10: LGTM! 적절한 UI 상태 설계로그인 화면의 UI 상태를 잘 표현한 data class입니다:
- 모든 프로퍼티에 기본값 제공으로 초기화 간편
- Immutable 속성(val) 사용으로 안전한 상태 관리
- 자동 로그인 및 토큰 검증 상태를 포함하여 완전한 UI 상태 표현
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt (1)
14-38: 초기 데이터 로드 누락 확인 필요ViewModel이 생성될 때
loadPosts()가 자동으로 호출되지 않습니다. 현재 구현에서는:
_uiState가Loading상태로 초기화됨- 실제 데이터 로드는 사용자가
refresh()를 호출해야 실행됨이것이 의도된 동작인지 확인이 필요합니다. 일반적으로 목록 화면은 진입 시 자동으로 데이터를 로드합니다.
초기 데이터 로드가 필요한 경우, init 블록을 추가하세요:
@HiltViewModel class PostListViewModel @Inject constructor( private val repository: PostRepository ) : ViewModel() { private val _uiState = MutableStateFlow<PostListUiState>(PostListUiState.Loading) val uiState: StateFlow<PostListUiState> = _uiState.asStateFlow() + + init { + loadPosts() + } private fun loadPosts() {혹은 화면 진입 시
refresh()를 명시적으로 호출하는 방식을 사용 중이라면 현재 구현이 적절합니다.app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepository.kt (1)
3-10: LGTM! 잘 설계된 토큰 관리 인터페이스토큰 관리에 필요한 모든 기능을 포함한 깔끔한 인터페이스입니다:
- 토큰 저장/조회/삭제 기능 제공
- 자동 로그인 설정 관리
- 모든 메서드가 suspend 함수로 선언되어 비동기 I/O 지원
DataStore를 사용한 구현체와 잘 연동될 것으로 예상됩니다.
app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt (2)
3-6: 로그인 요청/응답 모델 import 구성 적절
LoginRequest,LoginResponse를 공용으로 두고 API에서 재사용하는 구조 괜찮습니다. 인증 관련 모델이 한 곳에 모여 있어 유지보수에 유리해 보입니다. 빌드 및 직렬화 설정만 한 번 확인해 주세요.
57-70: 인증 API 시그니처 재확인 제안 (signup/login/validateToken)
signup과login이 둘 다LoginRequest를 사용하고LoginResponse를 반환하는 설계는 단순해서 좋지만, 서버 스펙이 동일 payload를 기대하는지 한 번만 다시 확인해 주세요.validateToken이BaseResponse<Boolean>을 반환하는 것도 충분하지만, 추후에 만료 시간 등 추가 정보가 필요해질 수 있으니 별도의 응답 DTO(TokenValidationResponse등)를 고려해 볼 여지는 있습니다.현재 구현 자체는 문제 없어 보입니다.
app/src/main/java/com/example/kuit6_android_api/ui/navigation/Routes.kt (1)
15-18: LoginRoute 추가 및 data object 사용 관련
PostEditRoute와 동일하게LoginRoute에도@Serializable을 붙이고data object로 정의한 점이 타입 세이프 네비게이션과 잘 맞습니다. 다만data object는 비교적 최신 Kotlin 버전에서만 지원되니, 프로젝트 Kotlin/Gradle 플러그인 버전이 이를 지원하는지 한 번만 확인해 주세요.app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt (1)
1-14: PostCreateUiState 상태 설계 적절로딩/성공/에러를 하나의
sealed class로 묶은 구조가 명확하고,Success에uploadedImageUrl과isUploading을 함께 둔 것도 실사용 시 편리해 보입니다. 초기 상태를 어디서 어떻게 둘지(ViewModel 등)만 일관되게 가져가면 될 것 같습니다.app/src/main/java/com/example/kuit6_android_api/MainActivity.kt (1)
81-103: 권한 체크 로직 변경 없음 확인
checkAndRequestPermission의 분기 구조(권한 보유 / rationale 필요 / 최초 요청)는 이전과 동일한 로직으로 보이고, 변경은 주로 포맷팅 수준이라 동작상 문제는 없어 보입니다. 실제 단말에서 Android 12/13 이상 모두 한번씩만 권한 플로우 확인해 주시면 좋겠습니다.app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (3)
3-21: NavGraph에 SnackbarHostState 및 startDestination 타입 추가 적절
NavGraph가snackBarState: SnackbarHostState를 인자로 받아 각 화면으로 전달하는 구조가 깔끔하고,startDestination을Any = PostListRoute로 두어 타입 세이프 라우팅과 호환되도록 한 점도 좋아 보입니다. 네비게이션 라이브러리 버전과toRoute확장 함수가 현재 설정과 잘 맞는지만 한번만 확인해 주세요.
26-38: PostListScreen에 login 네비게이션 및 hiltViewModel 연결 적절
onLoginClick에서LoginRoute로 네비게이션을 추가하고,viewModel = hiltViewModel()로 DI를 연결한 부분이 전체 인증 플로우와 잘 맞습니다. 런타임에서 뒤로가기 시 Login → PostList로 자연스럽게 돌아오는지만 UX 차원에서 한번만 확인해 보시면 좋겠습니다.
41-67: PostDetail/PostCreate 화면에 viewModel 및 snackBarState 전달 구조 좋음두 화면 모두
hiltViewModel()을 통해 ViewModel을 주입하고, 상위에서 받은snackBarState를 그대로 넘겨주는 패턴이 일관적입니다. 각 Screen 쪽에서 Snackbar를 실제로 띄우는 로직만 잘 연결되어 있다면 구조적으로 문제 없어 보입니다.app/src/main/java/com/example/kuit6_android_api/data/di/RepositoryModule.kt (1)
17-35: Repository 바인딩 구성이 Hilt 패턴에 잘 맞습니다각 Repository 인터페이스와 구현체가
@Binds+@Singleton으로 일관되게 연결되어 있고,SingletonComponent에 설치된 것도 적절해 보입니다. 현재 구조 그대로 사용해도 무방해 보입니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt (1)
20-40: Hilt + StateFlow 를 이용한 편집 화면 상태 관리 구성이 깔끔합니다.단일
PostEditUiState로 로딩/성공/에러를 모두 표현하고,viewModelScope.launch안에서만_uiState를 갱신하는 패턴이 일관적이라 이후 화면 확장·테스트에도 잘 맞을 것 같습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt (2)
25-38: 입력 필드 변경을StateFlow+copy로 처리하는 패턴이 좋습니다.
onIdChanged/onPasswordChanged에서_uiState.update { it.copy(...) }를 쓰는 구조가 명확하고, 나중에 필드가 늘어나더라도 확장하기 쉬운 형태입니다.
82-141: 토큰 검증 플로우와 자동 로그인 초기화 로직이 일관되게 잘 구성돼 있습니다.
validateToken에서 검증 중/성공/실패에 따라 버튼 텍스트와isAutoLogin을 명시적으로 갱신하고,init블록에서 DataStore 의 자동 로그인 값을 읽어 초기 UI 를 맞추는 구조가 자연스럽습니다. 나중에 에러 메시지나 로딩 인디케이터를 더 추가하기도 쉬워 보입니다.app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt (1)
18-57: Preferences DataStore 를 이용한 토큰/자동 로그인 저장 구현이 단순하고 명확합니다.토큰과 자동 로그인 여부를 각각 별도 키로 관리하고,
get*계열에서 기본값까지 처리해 주어서 ViewModel 이나 인터셉터 입장에서 사용하기 편한 구조입니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt (1)
31-51: 게시글 생성 플로우의 상태 전이가 단순해서 추적하기 좋습니다.
createPost에서Loading → Success/Error로만 상태를 전이시키고, 성공 시onSuccess콜백을 분리해 둔 구조 덕분에 UI 쪽에서 네비게이션 처리와 스낵바 노출을 유연하게 조합할 수 있을 것 같습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt (1)
57-106: 상태 기반 렌더링과 삭제 후 스낵바 처리 흐름이 자연스럽습니다.
uiState의Loading/Success/Error에 따라 서로 다른 UI 를 그리는 구조가 명확하고, 삭제 확인 다이얼로그에서 성공 콜백 안에서 스낵바를 띄운 뒤onNavigateBack()을 호출하는 플로우도 사용자 경험 측면에서 좋습니다.Also applies to: 226-241
| package com.example.kuit6_android_api | ||
|
|
||
| import android.app.Application | ||
| import androidx.lifecycle.ViewModelProvider.NewInstanceFactory.Companion.instance |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사용되지 않는 잘못된 import를 제거해주세요.
ViewModelProvider.NewInstanceFactory.Companion.instance import는 이 파일에서 사용되지 않으며, 실제로는 companion object의 instance 프로퍼티를 사용하고 있습니다. 이 import는 혼란을 야기할 수 있으므로 제거해야 합니다.
다음과 같이 import를 제거하세요:
package com.example.kuit6_android_api
import android.app.Application
-import androidx.lifecycle.ViewModelProvider.NewInstanceFactory.Companion.instance
import dagger.hilt.android.HiltAndroidApp📝 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.
| import androidx.lifecycle.ViewModelProvider.NewInstanceFactory.Companion.instance | |
| import androidx.lifecycle.ViewModelProvider.NewInstanceFactory.Companion.instance |
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/App.kt around line 4, remove
the unused and incorrect import
'androidx.lifecycle.ViewModelProvider.NewInstanceFactory.Companion.instance' —
delete this import line so the file relies on the companion object's instance
property directly and does not import a non-used symbol that causes confusion.
| override fun intercept(chain: Interceptor.Chain): Response { | ||
| val token = runBlocking { | ||
| //블록이 완료될 때까지 intercept를 호출한 스레드의 실행 멈춤 | ||
| tokenRepository.getToken() | ||
| } | ||
|
|
||
| val request = chain.request().newBuilder() | ||
| if(!token.isNullOrEmpty()){ | ||
| request.addHeader("Authorization", "Bearer $token") | ||
| } | ||
| return chain.proceed(request.build()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: 요청 스레드에서 runBlocking 사용으로 인한 성능 문제
Interceptor의 intercept 메서드는 네트워크 요청 스레드에서 실행되며, 여기서 runBlocking을 사용하면 해당 스레드가 코루틴 완료까지 차단됩니다. 이는 다음과 같은 심각한 문제를 발생시킬 수 있습니다:
- 성능 저하: 네트워크 스레드 풀의 스레드가 블로킹되어 동시 요청 처리 능력 감소
- ANR 위험: 메인 스레드에서 호출 시 ANR 발생 가능
- 데드락 위험: DataStore I/O가 지연되면 요청이 장시간 대기
다음 중 하나의 해결 방법을 고려하세요:
해결 방법 1 (권장): 동기 토큰 캐시 사용
TokenRepository에 메모리 캐싱을 추가하여 동기적으로 토큰을 반환:
@Singleton
class AuthInterceptor @Inject constructor(
private val tokenRepository: TokenRepository
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = tokenRepository.getTokenSync() // 동기 메서드 추가
val request = chain.request().newBuilder()
if (!token.isNullOrEmpty()) {
request.addHeader("Authorization", "Bearer $token")
}
return chain.proceed(request.build())
}
}해결 방법 2: Authenticator 사용
OkHttp의 Authenticator를 사용하여 401 응답 시에만 토큰 재인증:
class TokenAuthenticator @Inject constructor(
private val tokenRepository: TokenRepository
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
val token = runBlocking { tokenRepository.getToken() }
return response.request.newBuilder()
.header("Authorization", "Bearer $token")
.build()
}
}🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/api/AuthInterceptor.kt
around lines 18-29, the interceptor currently uses runBlocking to fetch the
token which blocks the network thread; replace this with a non-blocking
synchronous token access by adding a synchronous cache accessor to
TokenRepository (e.g., getTokenSync() that reads an in-memory cached token
updated on login/refresh) and call that from intercept(), or alternatively
remove token fetch from intercept and implement an OkHttp Authenticator that
performs suspended token refresh logic (runBlocking only inside the
Authenticator if unavoidable) and attaches the Authorization header in intercept
using the synchronous cached token; ensure TokenRepository is updated to
maintain the in-memory cache and getTokenSync() is thread-safe.
| @@ -0,0 +1,63 @@ | |||
| package com.example.kuit6_android_api.data.di | |||
|
|
|||
| import com.example.kuit6_android_api.BuildConfig | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BODY 레벨 HttpLoggingInterceptor로 인한 토큰/민감 정보 노출 위험
현재 HttpLoggingInterceptor의 레벨을 항상 Level.BODY로 두고 있어, 요청/응답 바디와 헤더(Authorization 포함)가 모두 로그로 남습니다.
특히 로그인/토큰 API를 다루는 앱에서 릴리즈 빌드까지 BODY 로그가 켜져 있으면 보안·프라이버시 이슈가 될 수 있습니다.
빌드 타입에 따라 로그 레벨을 분기하는 것을 강하게 추천드립니다. 예:
- fun provideLoginInterceptor(): HttpLoggingInterceptor =
- HttpLoggingInterceptor().apply {
- level = HttpLoggingInterceptor.Level.BODY
- }
+ fun provideLoginInterceptor(): HttpLoggingInterceptor =
+ HttpLoggingInterceptor().apply {
+ level = if (BuildConfig.DEBUG) {
+ HttpLoggingInterceptor.Level.BODY
+ } else {
+ HttpLoggingInterceptor.Level.NONE
+ }
+ }이렇게 하면 개발 환경에서는 상세 로그를 유지하면서, 운영 환경에서는 민감 정보가 로그에 남지 않도록 할 수 있습니다.
Also applies to: 22-27
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/di/NetworkModule.kt
around lines 3 and 22-27, the HttpLoggingInterceptor is currently set to
Level.BODY which logs headers and bodies (including Authorization tokens) and
can leak sensitive data; change it to choose the logging level based on the
build type (e.g., if BuildConfig.DEBUG use Level.BODY or Level.HEADERS for
development, otherwise use Level.NONE or Level.BASIC for release), and ensure
headers containing sensitive info are not logged in release builds by
conditionally configuring the interceptor accordingly.
| when (uiState) { | ||
| is PostListUiState.Loading -> { | ||
| CircularProgressIndicator() | ||
| } | ||
|
|
||
| is PostListUiState.Success -> { | ||
| val posts = (uiState as PostListUiState.Success).posts | ||
| LazyColumn( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .padding(paddingValues) | ||
| .background(MaterialTheme.colorScheme.background), | ||
| contentPadding = PaddingValues(horizontal = 16.dp, vertical = 12.dp), | ||
| verticalArrangement = Arrangement.spacedBy(16.dp) | ||
| ) { | ||
| items(posts) { post -> | ||
| PostItem( | ||
| post = post, | ||
| onClick = { onPostClick(post.id) } | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| is PostListUiState.Error -> { | ||
| // Error 상태 처리 (practice-only와 동일하게 빈 상태) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
로딩/에러 상태에서 padding 미적용과 에러 UI 부재로 사용자 경험이 떨어질 수 있습니다.
Success분기만padding(paddingValues)와 배경색을 적용하고 있고,Loading의CircularProgressIndicator()와Error분기는paddingValues를 전혀 사용하지 않아TopAppBar바로 아래에 붙어서 그려질 수 있습니다.Error분기는 주석만 있고 실제 UI가 없어, 네트워크 실패 시 사용자가 빈 화면만 보게 되는 문제가 있습니다.
아래처럼 Box로 감싸 padding을 적용하고, 에러 메시지를 노출하는 쪽을 제안합니다(텍스트/스타일은 예시입니다).
- ) { paddingValues ->
- when (uiState) {
- is PostListUiState.Loading -> {
- CircularProgressIndicator()
- }
+ ) { paddingValues ->
+ when (uiState) {
+ is PostListUiState.Loading -> {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ }
@@
- is PostListUiState.Error -> {
- // Error 상태 처리 (practice-only와 동일하게 빈 상태)
- }
+ is PostListUiState.Error -> {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "게시글을 불러오지 못했습니다. 다시 시도해 주세요.",
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ }위 변경을 적용하려면 다음 import도 함께 필요합니다:
import androidx.compose.foundation.layout.Box
import androidx.compose.ui.Alignment
---
🏁 Script executed:
```shell
#!/bin/bash
# Find PostListScreen.kt and display content around lines 76-103
find . -name "PostListScreen.kt" -type f 2>/dev/null | head -5
Length of output: 82
🏁 Script executed:
#!/bin/bash
# Display the PostListScreen.kt file with line numbers around the target lines
if [ -f "app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt" ]; then
sed -n '1,20p' "app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt"
echo "---"
sed -n '70,110p' "app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt"
fiLength of output: 2218
🏁 Script executed:
#!/bin/bash
# Check what Compose imports are currently in the file
if [ -f "app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt" ]; then
grep -n "^import" "app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt" | head -30
fiLength of output: 1555
🏁 Script executed:
#!/bin/bash
# Search for PostListUiState definition to understand its structure
find . -name "*.kt" -type f 2>/dev/null | xargs grep -l "sealed class PostListUiState\|data class.*PostListUiState" 2>/dev/null | head -3Length of output: 82
🏁 Script executed:
#!/bin/bash
# Display PostListUiState definition if found
find . -name "*.kt" -type f 2>/dev/null -exec grep -l "sealed class PostListUiState" {} \; | head -1 | xargs cat 2>/dev/null | head -50Length of output: 362
🏁 Script executed:
#!/bin/bash
# Verify Box and Alignment are standard Compose components that need to be imported
echo "Box and Alignment are from androidx.compose.foundation.layout package"
echo "These are standard Compose components available in standard Compose library"Length of output: 145
로딩/에러 상태 padding 미적용과 에러 UI 부재 문제 확인됨 - 제안된 수정 필요
코드 검증 결과 review comment의 지적이 모두 타당합니다:
-
Padding 미적용 확인:
Success분기만.padding(paddingValues)를 적용 중이며,Loading과Error분기는 패딩 없이 화면 상단에 렌더링됩니다. -
에러 UI 부재 확인:
Error분기가 비어있으며,PostListUiState.Error가message: String속성을 가지고 있음에도 이를 표시하지 않습니다. -
필요한 import 확인: 현재 파일에
Box와Alignment가 import되지 않았으므로, 제안된 수정 적용 시 두 import을 추가해야 합니다.
제안된 수정사항(Box 래퍼 적용, 에러 메시지 표시, import 추가)을 적용하시기 바랍니다.
| fun updatePost( | ||
| postId: Long, | ||
| title: String, | ||
| content: String, | ||
| imageUrl: String? = null, | ||
| onSuccess: () -> Unit = {} | ||
| ) { | ||
| viewModelScope.launch { | ||
| _uiState.value = PostEditUiState.Loading | ||
| repository.updatePost(postId, title, content, imageUrl) | ||
| .onSuccess { | ||
| val currentState = _uiState.value | ||
| if (currentState is PostEditUiState.Success) { | ||
| _uiState.value = currentState.copy(uploadedImageUrl = null) | ||
| } | ||
| onSuccess() | ||
| } | ||
| .onFailure { error -> | ||
| _uiState.value = PostEditUiState.Error( | ||
| message = error.message ?: "error" | ||
| ) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
updatePost 내 uploadedImageUrl 초기화 로직이 실제로는 실행되지 않을 가능성이 큽니다.
_uiState를 먼저 Loading으로 덮어쓴 뒤에 currentState를 읽기 때문에, currentState가 Success일 확률이 거의 없어 uploadedImageUrl 초기화 분기가 사실상 죽은 코드가 됩니다. 의도에 따라 (1) 이 블록을 제거하고 외부에서 clearUploadedImageUrl()을 호출하거나, (2) 성공 이후에 실제로 Success 상태를 다시 만들도록 구조를 조정하는 쪽을 고려해 보시면 좋겠습니다.
검증을 위해 코드베이스를 살펴보겠습니다.
---
🏁 Script executed:
```shell
#!/bin/bash
# PostEditViewModel.kt 파일 찾기 및 내용 확인
find . -name "PostEditViewModel.kt" -type f 2>/dev/null | head -5
Length of output: 88
🏁 Script executed:
#!/bin/bash
# PostEditUiState 클래스 정의 찾기
find . -name "*UiState.kt" -type f 2>/dev/null | grep -i "post" | head -5Length of output: 415
🏁 Script executed:
#!/bin/bash
# 해당 파일의 전체 내용 확인
find . -path "*/post/viewmodel/PostEditViewModel.kt" -type f -exec cat {} \;Length of output: 4175
🏁 Script executed:
#!/bin/bash
# PostEditUiState 정의 확인
rg -n "class PostEditUiState" --type kotlin -A 20Length of output: 1506
🏁 Script executed:
#!/bin/bash
# clearUploadedImageUrl 함수 사용 패턴 확인
rg -n "uploadedImageUrl\|clearUploadedImageUrl" --type kotlin -B 2 -A 2Length of output: 0
호출 측 코드를 확인하여 updatePost 후 상태 처리 방식을 검증하겠습니다.
🏁 Script executed:
#!/bin/bash
# updatePost 호출 위치 찾기
rg -n "updatePost" --type kotlin -B 3 -A 3Length of output: 5856
🏁 Script executed:
#!/bin/bash
# PostEditViewModel 주입 및 사용 패턴 확인
rg -n "PostEditViewModel" --type kotlin -B 2 -A 2 | grep -v "test"Length of output: 2421
🏁 Script executed:
#!/bin/bash
# onSuccess 콜백에서 어떤 작업을 하는지 확인
find . -path "*/post/*" -name "*.kt" -type f ! -path "*/test/*" -exec grep -l "updatePost\|PostEditViewModel" {} \; | head -10Length of output: 262
PostEditScreen을 확인하여 최종 검증을 마무리하겠습니다.
🏁 Script executed:
#!/bin/bash
# PostEditScreen.kt의 updatePost 호출 주변 전체 흐름 확인
cat app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt | sed -n '240,270p'Length of output: 1622
🏁 Script executed:
#!/bin/bash
# PostEditScreen에서 uiState 구독하는 부분 확인
rg -n "uiState\|PostEditUiState" app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt -B 2 -A 2Length of output: 0
updatePost 메서드의 uploadedImageUrl 초기화 로직이 실행되지 않습니다.
라인 50에서 _uiState.value = PostEditUiState.Loading으로 상태를 설정한 후, 라인 53에서 currentState를 읽으므로 currentState는 항상 Loading 상태입니다. 따라서 라인 54의 if (currentState is PostEditUiState.Success) 조건이 항상 거짓이 되어, 라인 55-57의 uploadedImageUrl 초기화 코드는 실행될 수 없는 죽은 코드가 됩니다.
해결 방안:
- (1) 이 블록을 제거하고 성공 시
onSuccess콜백 내에서clearUploadedImageUrl()을 호출하거나 - (2) 성공 후에 업데이트된
postDetail을 포함한Success상태로 명시적으로 업데이트
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt
around lines 42 to 65, the code reads _uiState as Loading before checking for
Success so the uploadedImageUrl reset branch never runs; either remove that dead
branch and call clearUploadedImageUrl() from the onSuccess callback after
repository.updatePost succeeds, or after a successful update explicitly set
_uiState to a PostEditUiState.Success (including the updated postDetail) with
uploadedImageUrl = null so the state reflects success and the uploadedImageUrl
is cleared.
https://github.com/1hyok/ProjectSettings
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.