-
Notifications
You must be signed in to change notification settings - Fork 2
Refactor/#117 mypage #118
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
Refactor/#117 mypage #118
Conversation
Adds Dagger/Hilt modules for dependency injection. - `AppModule.kt`: Provides `TokenStorage` and `TokenManager` as singletons. - `NetworkModule.kt`: Provides Retrofit instances (authenticated and non-authenticated) and various API services (`OnboardingService`, `AuthService`, `EmotionService`, `MypageService`, `HomeService`).
- Integrate Hilt for dependency injection in `HomeViewModel` and `HomeOptionViewModel`, removing the custom `HomeViewModelFactory`. - Refactor `HomeViewModel` to use a single `HomeUiState` data class, adopting an MVI-like pattern to manage UI state. - Convert `HomeOptionViewModel`'s API calls from Retrofit `Call` to `suspend` functions within a `viewModelScope`. - Update `HomeFragment` and `HomeOptionFragment` to use Hilt's `by viewModels()` for ViewModel instantiation. - Remove direct `LiveData` observers for individual UI elements in `HomeFragment` in favor of observing the single `HomeUiState`. - Add a new `HomeUiState.kt` file to define the state for the home screen. - Make `HomeRepository` a singleton provided by Hilt.
- Deleted the unused `HomeViewModelFactory` import from several fragments. - Commented out the `getCardDetail` function and its usage in `CardViewModel` as it is no longer needed.
The `AuthViewModelFactory` class has been deleted as it is no longer in use.
- Set test runner to be chosen per test - Set app module target bytecode version to 21 - Remove `resolveExternalAnnotations` option
- Apply Hilt for dependency injection in `MypageFragment` and `MypageViewModel`. - Introduce `MypageUiState` to manage the UI state in a single object. - Refactor network calls in `MypageViewModel` to use suspend functions and coroutines instead of Retrofit Callbacks. - Add suspend functions (`getMypageSuspend`, `logoutSuspend`, `signOutSuspend`) to the respective service interfaces. - Remove manual ViewModelFactory instantiation in `MypageFragment`.
- Add `ProfileRepository` to abstract data sources. - Introduce `ProfileUiState` to manage UI state in the `ProfileViewModel`. - Refactor `ProfileViewModel` to use Hilt for dependency injection, `viewModelScope` for coroutines, and the new `ProfileRepository`. - Convert `ProfileFragment` to use Hilt (`@AndroidEntryPoint`) for ViewModel injection. - Remove manual ViewModel factory instantiation. - Update API calls to be asynchronous using coroutines instead of Retrofit Callbacks.
Add `@AuthRetrofit` and `@NonAuthRetrofit` qualifiers to distinguish between authenticated and non-authenticated Retrofit instances.
- Refactored `SignupNicknameViewModel` to use Hilt for dependency injection. - Replaced direct Retrofit calls with a repository pattern (`ProfileRepository`). - Converted asynchronous API calls from `Callback` to `suspend` functions and `viewModelScope` coroutines. - Introduced `ProfileUiState` to manage the view's state in a more structured way. - Updated `OnboardingService` to include `suspend` versions of network methods. - Modified `SignupNicknameFragment` to use the Hilt-injected ViewModel.
MainActivity에 @androidentrypoint 어노테이션을 추가하여 Hilt 종속성 주입을 활성화합니다.
`AuthService`와 `EmotionService`의 API 호출 함수를 `suspend` 함수로 변경하여 코루틴을 지원하도록 수정했습니다. - `AuthService`에 로그아웃 및 회원탈퇴를 위한 `suspend` 함수 추가 - `EmotionService`에 감정 기록을 위한 `suspend` 함수 추가
Walkthrough프로젝트 전반에 Hilt DI를 도입하고 Retrofit 호출을 suspend 기반으로 이중화했습니다. Home/Mypage/Profile 영역은 UI 상태(data class) 중심으로 ViewModel을 재구성했고, 다수의 Fragment와 Activity에 @androidentrypoint를 적용했습니다. 기존 ViewModelFactory는 제거/비활성화되었으며, 네트워크 모듈과 Qualifier를 추가했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor UI
participant VM as ViewModel (e.g., HomeOptionViewModel/MypageViewModel)
participant SVC as Retrofit Service
participant TM as TokenManager
UI->>VM: 사용자 액션 트리거
VM->>SVC: suspend API 호출
alt HTTP 200
SVC-->>VM: Response(success, body)
VM-->>UI: 상태 업데이트
else HTTP 401
SVC-->>VM: Response(401)
VM->>TM: refreshToken()
alt 리프레시 성공
TM-->>VM: 새 토큰 적용
VM->>SVC: API 재시도
SVC-->>VM: Response(...)
VM-->>UI: 상태 업데이트/오류 처리
else 리프레시 실패
TM-->>VM: 실패
VM-->>UI: 인증 오류 상태 설정
end
else 기타 에러/예외
SVC-->>VM: Response(error) 또는 예외
VM-->>UI: 오류 상태 설정
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 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 Pre-merge checks❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
|
|
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: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupStatusFragment.kt (2)
64-66: DataBinding lifecycleOwner 설정 위치/대상 부적절Fragment에서는
binding.lifecycleOwner = viewLifecycleOwner가 권장입니다. 현재this를 사용하고 있어 생명주기 불일치/누수 위험이 있습니다. onViewCreated에서 설정해 주세요.- binding.viewModel = signUpStatusViewModel - binding.lifecycleOwner = this + binding.viewModel = signUpStatusViewModelFragment 하단 onViewCreated에 추가:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.lifecycleOwner = viewLifecycleOwner ... }
111-121: 긴급: 로그에서 액세스/리프레시 토큰(민감정보) 완전 삭제 필요로그에 accessToken / refreshToken / token.accessToken이 평문으로 출력되고 있습니다. 즉시 삭제하거나 마스킹(또는 존재 여부만 boolean으로 기록)하세요.
문제 발견 위치:
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/LoginViewModel.kt:65, 73, 121, 159
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageViewModel.kt:45, 46, 81, 82
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupStatusFragment.kt:119
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/LoginFragment.kt:88, 110
권장 조치(간단 예시 — 토큰 제거):
- val accessToken = loginViewModel.oAuthAccessToken.value ?: "" ... - Timber.d("nickname: $nickname, status: $status, accessToken: $accessToken, signUpRequest: $signUpRequest") + Timber.d("nickname: %s, status: %s, signUpRequest: %s", nickname, status, signUpRequest)대체안: 토큰 대신 boolean 존재 여부만 로그(Timber.d("hasAccessToken=%b", accessToken.isNotBlank())) 또는 안전하게 마스킹. 모든 파일 수정 후 rg로 재검색해 노출 건수 0인지 확인하세요.
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/notice/NoticeFragment.kt (2)
15-15: 패키지 오타로 인한 컴파일 오류 가능성
com.toyou.toyouHoandroid...오타로 보입니다. 올바른 패키지로 수정해 주세요.-import com.toyou.toyouHoandroid.data.create.service.CreateService +import com.toyou.toyouandroid.data.create.service.CreateService
142-157: 클릭 시 중첩 LiveData observe로 다중 구독/중복 네비게이션 위험
onFriendCardItemClick마다observe를 추가합니다. 한 번만 소비하거나 현재 값만 사용하도록 변경하세요.- userViewModel.emotion.observe(viewLifecycleOwner) { emotion -> - if (emotion != null){ - userViewModel.cardId.observe(viewLifecycleOwner) { cardId -> - ... - } - } else { ... } - } + val emotion = userViewModel.emotion.value + if (emotion != null) { + val cardId = userViewModel.cardId.value + if (cardId == null) { + navController.navigate(R.id.action_navigation_notice_to_create_fragment) + } else { + navController.navigate(R.id.action_navigation_notice_to_modify_fragment) + } + } else { + Toast.makeText(requireContext(), "감정 우표를 먼저 선택해주세요", Toast.LENGTH_SHORT).show() + }또는
observeOnce유틸을 사용하세요.app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeFragment.kt (2)
18-18: 오타로 인한 패키지 경로 오류로 컴파일 실패
com.toyou.toyouHoandroid...오타입니다. 컴파일 불가이므로 즉시 수정 필요.-import com.toyou.toyouHoandroid.data.create.service.CreateService +import com.toyou.toyouandroid.data.create.service.CreateService
213-233: 클릭 시 중첩 LiveData 관찰로 중복 옵저버 누적 위험클릭할 때마다
emotion→cardId를 중첩 observe 하여 옵저버가 누적됩니다. 값의 현재 스냅샷만 필요하면 즉시 값(.value)을 읽고 분기하세요.- binding.homeMailboxIv.setOnClickListener { - userViewModel.emotion.observe(viewLifecycleOwner) { emotion -> - if (emotion != null){ - userViewModel.cardId.observe(viewLifecycleOwner) { cardId -> - if (cardId == null) { - navController.navigate(R.id.action_navigation_home_to_create_fragment) - cardViewModel.disableLock(false) - } - else { -// cardViewModel.getCardDetail(cardId.toLong()) - navController.navigate(R.id.action_navigation_home_to_modifyFragment) - cardViewModel.disableLock(true) - } - } - } - else{ - Toast.makeText(requireContext(), "감정 우표를 먼저 선택해주세요", Toast.LENGTH_SHORT).show() - } - } - } + binding.homeMailboxIv.setOnClickListener { + val emotion = userViewModel.emotion.value + if (emotion == null) { + Toast.makeText(requireContext(), "감정 우표를 먼저 선택해주세요", Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + val cardId = userViewModel.cardId.value + if (cardId == null) { + navController.navigate(R.id.action_navigation_home_to_create_fragment) + cardViewModel.disableLock(false) + } else { + // cardViewModel.getCardDetail(cardId.toLong()) + navController.navigate(R.id.action_navigation_home_to_modifyFragment) + cardViewModel.disableLock(true) + } + }app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionFragment.kt (1)
15-15: 오타로 인한 패키지 경로 오류로 컴파일 실패
com.toyou.toyouHoandroid...오타입니다. 즉시 수정 필요.-import com.toyou.toyouHoandroid.data.create.service.CreateService +import com.toyou.toyouandroid.data.create.service.CreateServiceapp/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileFragment.kt (1)
18-18: 오타로 인한 잘못된 패키지 import (컴파일 불가)
com.toyou.toyouHoandroid...→com.toyou.toyouandroid...로 수정하세요.-import com.toyou.toyouHoandroid.data.create.service.CreateService +import com.toyou.toyouandroid.data.create.service.CreateService
🧹 Nitpick comments (58)
.idea/compiler.xml (1)
3-7: IDE 전용 파일 커밋 최소화 권장.idea/compiler.xml 변경은 팀별 IDE/JDK 차이로 충돌이 잦습니다. Gradle 소스/타깃 호환성과 Kotlin jvmTarget만 관리하고 .idea 변경은 제외하거나 최소화하세요.
필요 시 .gitignore 조정 또는 공유가 필요한 최소한의 .idea 파일만 추립니다.
app/src/main/java/com/toyou/toyouandroid/presentation/base/MainActivity.kt (1)
19-23: @androidentrypoint 적용 LGTMHilt 진입점 지정 적절합니다. 추가로,
Timber.plant(DebugTree())는 Activity 재생성 시 중복 플랜팅될 수 있으니Application(@HiltAndroidApp)으로 이동을 고려하세요.app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageUiState.kt (1)
3-9: UI State 모델 추가 LGTM — nullable 최소화 제안프리미티브 성격의 값(
friendNum)은 기본값(예: 0)으로 두면 분기 단순화에 도움이 됩니다. 필요 시errorMessage/isRefreshing필드도 고려해 주세요.data class MypageUiState( - val userId: Int? = null, - val nickname: String? = null, - val status: String? = null, - val friendNum: Int? = null, + val userId: Int? = null, + val nickname: String? = null, + val status: String? = null, + val friendNum: Int = 0, val isLoading: Boolean = false )app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupStatusFragment.kt (2)
21-21: 불필요 주석 import 정리 OK
AuthViewModelFactory미사용 주석 처리 문제 없습니다. 최종적으로는 해당 의존성 제거(및 Hilt 마이그레이션 완료) 시 주석도 삭제 권장.
47-55: 수동 Factory/서비스 생성 → Hilt 주입으로 마이그레이션 권장PR 방향과 맞춰
TokenStorage/TokenManager/AuthService/FCMRepository를 Hilt로 주입하고by viewModels()를 사용하면 중복 제거 및 테스트 용이성이 개선됩니다.원하시면 Hilt 모듈/주입 예시 패치를 드리겠습니다.
app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/CardViewModel.kt (1)
166-236: 대규모 주석 블록 제거 및 대체 구현/이관 필요주석으로 남겨두면 부채만 커집니다. 1) 해당 로직을
HomeRepository/UseCase로 이관하고, 2) Hilt로 주입해getCardDetail을 복원하거나, 3) 기능 미사용이면 완전히 삭제하세요.
추가로, 네트워크 실패 시 재귀 재시도 구조는 백오프/최대 횟수 없이 호출 폭주 위험이 있으니 정책화가 필요합니다.-// fun getCardDetail(id : Long) { -// ... -// } +// TODO(#117): getCardDetail는 Hilt 주입된 Repository로 이관 후 백오프 포함 재시도 정책과 함께 복원하거나, 미사용 시 제거합니다.필요 시 코루틴
retry/지수 백오프 예시 제공 가능합니다.app/src/main/java/com/toyou/toyouandroid/presentation/fragment/notice/NoticeFragment.kt (1)
67-101: Fragment 내 수동 의존성 생성 → Hilt 전환 권장여기에서 여러 Service/Repository/Factory를 직접 생성하고 있습니다. DI 모듈로 제공하고
@AndroidEntryPoint+by viewModels()로 교체하면 생명주기/테스트/중복 측면에서 개선됩니다.원하시면 Notice 관련 DI 설계와 ViewModel 주입 패치를 제안하겠습니다.
app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/AuthService.kt (1)
29-33: suspend API 추가 LGTM — 이중 API 정리 제안동일 엔드포인트에
Call<>/suspend가 공존합니다. 마이그레이션 완료 시Call<>버전을@Deprecated처리 또는 제거하여 인터페이스를 단일화하세요.- @POST("auth/logout") - fun logout( + @Deprecated("Use logoutSuspend") + @POST("auth/logout") + fun logout( @Header("refreshToken") refreshToken: String ): Call<SignUpResponse> @@ - @DELETE("auth/unlink") - fun signOut( + @Deprecated("Use signOutSuspend") + @DELETE("auth/unlink") + fun signOut( @Header("refreshToken") refreshToken: String ): Call<SignUpResponse>
suspend사용부에서 예외/비성공 코드 처리(isSuccessful분기) 일관성을 확인해 주세요.Also applies to: 44-47
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileUiState.kt (3)
4-11: 하드코딩된 UI 문자열은 string 리소스로 이전 권장"title", "duplicateCheckMessage", "0/15" 등의 하드코딩은 i18n/번역과 테스트에 불리합니다. ViewModel 상태에는 의미값만 두고, 표시 문자열은 View/Fragment에서 리소스로 조합하는 형태로 정리해 주세요.
- val title: String = "회원가입", - val textCount: String = "0/15", - val duplicateCheckMessage: String = "중복된 닉네임인지 확인해주세요", + // View에서 string 리소스로 조합 + val titleResId: Int = R.string.signup_title, + val nicknameLength: Int = 0, + val nicknameMaxLength: Int = 15, + val duplicateCheckMessageResId: Int = R.string.nickname_duplicate_check_prompt,
7-12: status String과 selectedStatusType 이중 소스오브트루스 제거status(String)와 selectedStatusType(StatusType?)가 동일 개념을 중복 표현합니다. 한쪽만 유지하고, 필요한 경우 파생값으로 변환하세요. 중복 상태는 버그의 주요 원인입니다.
- val status: String = "", - val selectedStatusType: StatusType? = null + val selectedStatusType: StatusType? = null + // 필요 시 UI에서 selectedStatusType?.value 로 파생
8-11: 불리언 다발을 상태 모델로 통합 고려isDuplicateCheckEnabled / isNextButtonEnabled / isNicknameValid는 상호 의존적인 경우가 많습니다. DuplicateCheckState(Idle/Checking/Available/Taken) 등으로 캡슐화하면 분기와 테스트가 단순해집니다.
app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/OnboardingService.kt (4)
43-46: patchStatus 응답 타입 재확인 요청 (닉네임 응답 사용 의문)*PATCH users/status가 PatchStatusRequest를 받고도 Response를 반환하고 있습니다. 실제 API 스펙이 닉네임 응답을 재사용하는지 확인 부탁드립니다. 별도 PatchStatusResponse가 있다면 타입 불일치로 런타임 파싱 오류가 납니다.
16-21: 비동기(suspend) 이행 이후, Call 기반 API는 @deprecated로 안내혼재 기간 동안 명시적 디프리케이션으로 사용처 정리를 빠르게 할 수 있습니다.
@GET("users/nickname/check") - fun getNicknameCheck( + @Deprecated("Use getNicknameCheckSuspend") + fun getNicknameCheck( @Query("nickname") nickname: String, @Query("userId") userId: Int ): Call<NicknameCheckResponse>동일하게 patchNickname, patchStatus에도 적용 부탁드립니다.
22-26: suspend 반환형 Response 사용 기준 통일 제안HTTP 메타데이터가 필요 없다면
suspend fun ...(): NicknameCheckResponse로 단순화하는 편이 소비측 에러 처리와 테스트가 쉬워집니다. 전역적으로 Response vs T 기준을 정해 일관성 유지 바랍니다.
16-16: 엔드포인트 경로 표기 일관화(선행 슬래시 혼재)본 파일은 상대 경로("users/..."), 다른 서비스 파일은 절대경로("/users/...")를 사용합니다. Retrofit BaseUrl 규칙상 동작은 하지만, 팀 규칙으로 하나로 통일하세요.
Also applies to: 22-22, 28-28, 33-33, 38-38, 43-43
app/src/main/java/com/toyou/toyouandroid/data/mypage/service/MypageService.kt (1)
10-14: Call/suspend 혼재 단계에서 가이드 추가 및 경로 통일
- getMypage()는 @deprecated로 안내해 사용처 정리를 가속하세요.
- 경로 표기("/users/mypage")를 다른 서비스와 통일(선행 슬래시 유무)해 주세요.
- @GET("/users/mypage") - fun getMypage(): Call<MypageResponse> + @GET("users/mypage") + @Deprecated("Use getMypageSuspend") + fun getMypage(): Call<MypageResponse> - @GET("/users/mypage") - suspend fun getMypageSuspend(): Response<MypageResponse> + @GET("users/mypage") + suspend fun getMypageSuspend(): Response<MypageResponse>app/src/main/java/com/toyou/toyouandroid/domain/home/repository/HomeRepository.kt (1)
7-14: DI 전환은 깔끔합니다. 퍼블릭 API 반환 타입 명시 고려@singleton + @Inject 전환 LGTM. 다만 퍼블릭 suspend 함수의 반환 타입을 명시하면 (예: DTO 또는 Result 래퍼) 호출부 계약이 명확해집니다.
- suspend fun getCardDetail(id: Long) = homeService.getCardDetail(id) + suspend fun getCardDetail(id: Long): CardDetailResponse = homeService.getCardDetail(id) - suspend fun getYesterdayCard() = homeService.getCardYesterday() + suspend fun getYesterdayCard(): YesterdayCardResponse = homeService.getCardYesterday()(실제 타입명에 맞춰 조정)
app/src/main/java/com/toyou/toyouandroid/data/emotion/service/EmotionService.kt (1)
16-24: POST/suspend 이행은 적절. 기존 Call API는 @deprecated로 표시이동 가이드가 있으면 호출부 마이그레이션이 빨라집니다.
@POST("users/emotions") - fun patchEmotion( + @Deprecated("Use patchEmotionSuspend") + fun patchEmotion( @Body emotion: EmotionRequest ): Call<EmotionResponse>또한 이 파일과 다른 서비스의 경로 표기(선행 슬래시) 규칙도 하나로 통일해 주세요.
app/src/main/java/com/toyou/toyouandroid/di/AppModule.kt (1)
24-31: TokenManager/TokenStorage는 @Inject 생성자로 단순화 가능 여부 검토두 클래스가 우리 코드 소유라면 생성자에 @Inject(및 @ApplicationContext) 붙여 Hilt가 자동 생성하도록 하고, 모듈에서는 불가피한 항목만 @provides로 남기는 편이 유지보수에 유리합니다.
// 예시: TokenStorage class TokenStorage @Inject constructor( @ApplicationContext private val context: Context )그 후 provideTokenStorage/provideTokenManager 제거 가능 여부 검토 바랍니다.
app/src/main/java/com/toyou/toyouandroid/di/RepositoryModule.kt (1)
15-21: @provides 대신 @Inject 생성자 또는 @BINDS(+인터페이스)로 단순화 제안ProfileRepository가 구체 클래스라면 생성자에 @Inject를 붙여 자동 주입이 가능합니다. 혹시 도메인 계층에 인터페이스가 있다면 ProfileRepositoryImpl로 분리하고 @BINDS를 사용하는 것이 관례에 더 가깝습니다.
- @Provides - @Singleton - fun provideProfileRepository( - onboardingService: OnboardingService - ): ProfileRepository { - return ProfileRepository(onboardingService) - } + // 대안 1) 구현 클래스에 @Inject constructor 추가 후 본 provider 삭제 + // 대안 2) 인터페이스가 있다면: + // @Binds @Singleton + // abstract fun bindProfileRepository(impl: ProfileRepositoryImpl): ProfileRepositoryapp/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/HomeViewModelFactory.kt (2)
3-10: 파일 내 모든 코드 주석 처리 상태에서 불필요한 import 잔존빌드에는 영향 없지만 불필요한 경고를 유발합니다. Hilt로 완전히 대체되었으므로 파일 자체 삭제 혹은 import 제거 + 폐기 주석만 남기는 것을 권장합니다.
-import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.toyou.toyouandroid.domain.home.repository.HomeRepository -import com.toyou.toyouandroid.presentation.fragment.emotionstamp.HomeOptionViewModel -import com.toyou.toyouandroid.presentation.fragment.home.HomeViewModel -import com.toyou.toyouandroid.presentation.fragment.mypage.ProfileViewModel -import com.toyou.toyouandroid.utils.TokenManager +/* Deprecated after Hilt migration. This file can be safely removed. */
11-37: 주석 블록 유지 대신 파일 제거 권장히스토리는 Git이 보존합니다. 주석으로 남겨두면 추후 리팩터링 시 혼란만 가중됩니다. 삭제를 제안합니다.
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeUiState.kt (1)
8-8: 하드코딩된 기본 문자열 제거기본값
"멘트"는 리소스 관리/다국어 측면에서 부적절합니다. 빈 문자열로 두고 UI에서 string resource를 세팅하세요.- val emotionText: String = "멘트", + val emotionText: String = "",app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeFragment.kt (3)
180-199: 접근성: ACTION_UP에서도 performClick 호출 필요현재
ACTION_CANCEL에서만v.performClick()을 호출합니다.ACTION_UP에서도 호출해 접근성 이벤트를 보장하세요.- MotionEvent.ACTION_UP, MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { + MotionEvent.ACTION_UP, MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { // 터치 시작 시 바텀 시트 핸들바 색깔 변경 binding.homeBottomSheetTouchBar.setBackgroundResource(R.drawable.next_button_enabled_roundly) - true + if (event.action == MotionEvent.ACTION_UP) v.performClick() + true }
318-332: ItemDecoration 중복 추가 방지
setupRecyclerView가 여러 번 호출되면 데코레이션이 누적됩니다. 추가 전에 카운트를 확인하세요.- binding.homeBottomSheetRv.addItemDecoration( - HomeBottomSheetItemDecoration( - horizontalSpaceHeight, - verticalSpaceHeight - ) - ) + if (binding.homeBottomSheetRv.itemDecorationCount == 0) { + binding.homeBottomSheetRv.addItemDecoration( + HomeBottomSheetItemDecoration( + horizontalSpaceHeight, + verticalSpaceHeight + ) + ) + }
73-91: DI 혼합 사용: Hilt 전환 진행 가이드
HomeViewModel은 Hilt 주입, 나머지는 수동 팩토리/정적 모듈을 혼용 중입니다. 중복 싱글톤/스코프 불일치 문제가 생길 수 있으니,NoticeViewModel,UserViewModel,CardViewModel,CardInfoViewModel및 관련 Repository/Service도 순차적으로 Hilt로 이관하세요.필요 시 제가
@Module/@Provides스텁을 생성해드릴 수 있습니다. 요청하시면 바로 드리겠습니다.Also applies to: 101-115
app/src/main/java/com/toyou/toyouandroid/domain/profile/repository/ProfileRepository.kt (1)
10-12: Hilt 일관성: 생성자 주입 전환 제안모듈에서
@Provides로 공급 중이더라도, 가능한 경우@Inject constructor로 단순화하여 테스트/교체 용이성을 높이세요.-import retrofit2.Response +import retrofit2.Response +import javax.inject.Inject @@ -class ProfileRepository( +class ProfileRepository @Inject constructor( private val onboardingService: OnboardingService ) {app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionFragment.kt (3)
39-39: 가시성 축소
navController는 외부 노출 필요 없어 보입니다.private로 축소하세요.- lateinit var navController: NavController + private lateinit var navController: NavController
142-147: 문자열 하드코딩 제거토스트 메시지는 string 리소스로 관리하세요. 신규 리소스(
home_emotion_once_per_day) 추가를 권장합니다.- Toast.makeText(requireContext(),"감정은 하루에 한 번만 선택할 수 있어요", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), getString(R.string.home_emotion_once_per_day), Toast.LENGTH_SHORT).show()
58-71: DI 혼합 사용: UserViewModel도 Hilt로 이관 권장이 프래그먼트도 Hilt 적용 상태입니다.
UserViewModel을by viewModels()또는by activityViewModels()와@HiltViewModel로 이관하면 팩토리/정적 모듈 의존이 줄어듭니다.app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionViewModel.kt (3)
31-62: 401 재인증 재시도 로직의 잠재적 무한 재귀/재진입 방지
refreshToken성공 시updateEmotion을 다시 호출하는 구조로, 반복 401 시 무한 재귀 가능성이 있습니다. 1회 재시도 가드 플래그를 추가하세요.- fun updateEmotion(emotionRequest: EmotionRequest) { + fun updateEmotion(emotionRequest: EmotionRequest, hasRetried: Boolean = false) { viewModelScope.launch { _isLoading.value = true _errorMessage.value = null @@ - val response = emotionService.patchEmotionSuspend(emotionRequest) + val response = emotionService.patchEmotionSuspend(emotionRequest) if (response.isSuccessful) { - _emotionResponse.postValue(response.body()) + _emotionResponse.value = response.body() Timber.tag("emotionResponse").d("emotionResponse: $response") } else { Timber.tag("API Error").e("Failed to update emotion. Code: ${response.code()}, Message: ${response.message()}") - if (response.code() == 401) { - tokenManager.refreshToken( - onSuccess = { updateEmotion(emotionRequest) }, + if (response.code() == 401 && !hasRetried) { + tokenManager.refreshToken( + onSuccess = { updateEmotion(emotionRequest, hasRetried = true) }, onFailure = { Timber.e("Failed to refresh token and update emotion") _errorMessage.value = "인증 실패. 다시 로그인해주세요." } ) } else { _errorMessage.value = "감정 업데이트에 실패했습니다." } } } catch (e: Exception) { Timber.tag("API Failure").e(e, "Error occurred during API call") _errorMessage.value = "네트워크 오류가 발생했습니다." } finally { _isLoading.value = false } } }
25-29: 초기값 명시 및 메인스레드에서 setValue 사용 일관화로딩/에러 LiveData 초기값을 명시하면 UI 상태 초기화가 간단해지고 NPE를 줄일 수 있습니다.
- private val _isLoading = MutableLiveData<Boolean>() + private val _isLoading = MutableLiveData(false) val isLoading: LiveData<Boolean> get() = _isLoading - private val _errorMessage = MutableLiveData<String?>() + private val _errorMessage = MutableLiveData<String?>(null) val errorMessage: LiveData<String?> get() = _errorMessage
37-43: postValue 대신 value 사용이미 코루틴 메인 컨텍스트에서 실행 중이라면
postValue대신value가 적합합니다.- _emotionResponse.postValue(response.body()) + _emotionResponse.value = response.body()app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameFragment.kt (2)
92-95: 매직 넘버 사용 및 API 시그니처 확인 필요checkDuplicate(1)의 상수 1은 의미가 불명확합니다. 실제 필요한 파라미터(닉네임/유저ID 등)로 교체하거나 내부에서 가져오도록 변경하세요.
12-26: 미사용/레거시 import와 주석 정리 제안수동 DI 관련 import(AuthService, HomeRepository, NetworkModule, TokenManager, TokenStorage)와 activityViewModels, ViewModelProvider가 남아 있습니다. 컴파일/린트 통과에는 영향 없으나 정리 권장합니다.
Also applies to: 50-52
app/src/main/java/com/toyou/toyouandroid/di/NetworkModule.kt (2)
17-24: 모듈명 혼동 방지 리팩터 제안장기적으로 객체명을 RetrofitModule(또는 NetworkDiModule) 등으로 변경하면 가독성과 충돌 위험이 줄어듭니다.
예:
-object NetworkModule { +object RetrofitModule {
21-27: 프로바이더 명확성 제안
provideRetrofit()는 @AuthRetrofit을 제공하므로provideAuthRetrofit()로 명명하면 더 명확합니다.app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileFragment.kt (3)
56-61: 토큰/레포 수동 생성 잔존 — DI 일관성 개선 제안현재 Fragment에서 Retrofit/TokenManager/CreateRepository를 수동 생성합니다. Hilt로 주입받거나 UserViewModel도 @hiltviewmodel로 전환하면 중복 인스턴스 생성을 피할 수 있습니다(인터셉터/쿠키 일관성 확보).
Also applies to: 64-67
148-155: 동시 네트워크 호출 및 UX 보완 제안닉네임/상태 변경 버튼 연타 방지(비활성화/로딩 표시)와 실패 처리 토스트/재시도 안내를 추가하면 UX가 개선됩니다.
64-67: userViewModel 미사용 — 제거 권장app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileFragment.kt에서 userViewModel이 선언(약 ln44)되고 ViewModelProvider로 생성(약 ln64–66)되지만 파일 내 바인딩/observe/참조가 없습니다. 미사용이면 선언과 생성 코드를 제거하세요.
userViewModel = ViewModelProvider( requireActivity(), UserViewModelFactory(createRepository,tokenManager) )[UserViewModel::class.java]app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageFragment.kt (1)
113-119: 문자열 하드코딩 i18n 개선 제안"친구 N명"은 plurals 리소스로 처리하세요(복수형/현지화 대비).
예: res/values/plurals.xml에 friends_count 추가 후
resources.getQuantityString(R.plurals.friends_count, n, n)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeViewModel.kt (3)
58-69: 실패 분기에서 isEmpty 갱신 누락API isSuccess=false일 때 isEmpty가 갱신되지 않아 UI 불일치가 생길 수 있습니다. 아래처럼 통일하세요.
- _uiState.value = _uiState.value?.copy(isLoading = false) + _uiState.value = _uiState.value?.copy( + isLoading = false, + isEmpty = true, + yesterdayCards = emptyList() + ) ... - _uiState.value = _uiState.value?.copy( - isLoading = false, - yesterdayCards = emptyList() - ) + _uiState.value = _uiState.value?.copy( + isLoading = false, + yesterdayCards = emptyList(), + isEmpty = true + )
44-79: 토큰 갱신 재시도 무한 루프 가능성 가드 추가refreshToken 성공 후 재요청이 반복적으로 isSuccess=false를 반환하면 루프가 생길 수 있습니다. 재시도 1회 가드 또는 백오프/회로차단을 추가하세요.
예시:
fun getYesterdayCard(retry: Boolean = true) { viewModelScope.launch { _uiState.value = _uiState.value?.copy(isLoading = true) try { val response = homeRepository.getYesterdayCard() if (response.isSuccess) { ... } else { _uiState.value = _uiState.value?.copy(isLoading = false, isEmpty = true, yesterdayCards = emptyList()) if (retry) tokenManager.refreshToken(onSuccess = { getYesterdayCard(false) }, onFailure = { ... }) } } catch (e: Exception) { ... } } }
59-68: 콜백 스레드 컨텍스트 주의refreshToken 콜백이 메인 스레드를 보장하지 않으면 LiveData 업데이트가 비메인에서 발생할 수 있습니다. 콜백 내부 업데이트를
viewModelScope.launch { ... }로 감싸는 것을 권장합니다.app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameViewModel.kt (4)
23-33: 호환성 LiveData 초기값/표현식 정합성 이슈
- nickname 기본값이 null입니다. 데이터바인딩에서 NPE/빈 문자열 처리 분기 발생 가능성이 높습니다. 기본값을 ""로 두는 편이 안전합니다.
- textCount 초기값은 "0/15"인데, updateTextCount에서는 "($count/15)"로 괄호가 붙습니다. 표기 통일을 권장합니다.
다음과 같이 최소 수정 제안합니다:
- val nickname: LiveData<String> = MutableLiveData() + val nickname: LiveData<String> = MutableLiveData("")또는 updateTextCount 표기만 통일:
- val countText = "($count/15)" + val countText = "$count/15"
57-67: 길이 초과 시 상호작용이 그대로 유지됨길이 15자 이상일 때 메시지만 바꾸고 다음 이동/중복확인 활성 상태는 그대로입니다. 사용자가 제한을 오해할 수 있으므로 버튼 비활성화를 함께 적용하는 편이 UX상 자연스럽습니다.
fun updateLength15(length: Int) { val messageType = if (length >= 15) { DuplicateCheckMessageType.LENGTH_EXCEEDED } else { DuplicateCheckMessageType.CHECK_REQUIRED } _duplicateCheckMessageType.value = messageType - _uiState.value = _uiState.value?.copy( - duplicateCheckMessage = messageType.message - ) + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = messageType.message, + isNextButtonEnabled = length < 15, + isDuplicateCheckEnabled = length in 1..14 + ) }
77-80: resetState 시 호환성 LiveData와의 상태 불일치_uiState만 초기화하고, 외부에 노출된 호환성 LiveData들은 초기화하지 않아 화면에 이전 상태가 남을 수 있습니다.
최소 수정으로 resetState에서 함께 초기화하세요:
fun resetState() { _uiState.value = ProfileUiState(title = "회원가입") _duplicateCheckMessageType.value = DuplicateCheckMessageType.CHECK_REQUIRED + (nickname as MutableLiveData).value = "" + (textCount as MutableLiveData).value = "0/15" + (duplicateCheckMessage as MutableLiveData).value = DuplicateCheckMessageType.CHECK_REQUIRED.message + (duplicateCheckMessageColor as MutableLiveData).value = 0xFF000000.toInt() + (isNextButtonEnabled as MutableLiveData).value = false + (nextButtonTextColor as MutableLiveData).value = 0xFFA6A6A6.toInt() + (nextButtonBackground as MutableLiveData).value = com.toyou.toyouandroid.R.drawable.next_button + (duplicateCheckButtonTextColor as MutableLiveData).value = 0xFFA6A6A6.toInt() + (duplicateCheckButtonBackground as MutableLiveData).value = com.toyou.toyouandroid.R.drawable.next_button }장기적으로는 호환성 LiveData를 모두 제거하고 uiState 단일 소스로 바인딩하거나, BindingAdapter/Transformations로 파생 값을 노출하는 구조를 권장합니다.
121-127: 색상 매핑 확인 요청 (가용/불가 메시지 색상)사용 가능 시 0xFFEA9797(핑크톤), 사용 불가 시 0xFFFF0000(레드)로 지정되어 있습니다. 일반적으로 성공/가용은 긍정 색(그린 등)을 사용합니다. 디자인 스펙과 일치하는지 확인 부탁드립니다. 또한 하드코딩된 색상은 color 리소스로 이관 권장합니다.
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageViewModel.kt (2)
114-123: 전체 상태 재생성으로 기존 UI 플래그 손실 가능성MypageUiState를 새 인스턴스로 교체하면, 화면에서 유지하던 플래그(예: 선택 상태 등)가 초기화될 수 있습니다. 필요한 필드만 copy로 갱신하세요.
- _uiState.value = MypageUiState( - userId = result.userId, - nickname = result.nickname, - status = result.status, - friendNum = result.friendNum, - isLoading = false - ) + _uiState.value = _uiState.value?.copy( + userId = result.userId, + nickname = result.nickname, + status = result.status, + friendNum = result.friendNum, + isLoading = false + )
27-35: 이벤트성 LiveData 초기값/재발행 전략 확인_logoutSuccess가 null에서 시작합니다. 관찰 측에서 null 처리 분기가 필요합니다. 초기 false 지정 또는 SingleLiveEvent/Channel 패턴 고려를 권장합니다. signOutSuccess도 동일.
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileViewModel.kt (8)
41-42: 초기 타이틀 오표기 가능성 ("회원가입")마이페이지/프로필 화면의 ViewModel인데 기본 타이틀이 "회원가입"입니다. 의도라면 무시해도 되나, 복붙 실수 가능성이 큽니다.
- _uiState.value = ProfileUiState(title = "회원가입") + _uiState.value = ProfileUiState(title = "프로필")
60-69: 길이 초과 시 버튼 활성 상태 유지Signup 쪽과 동일하게, 길이 15자 이상일 때 다음/중복확인 버튼 비활성화를 권장합니다.
_duplicateCheckMessageType.value = messageType - _uiState.value = _uiState.value?.copy( - duplicateCheckMessage = messageType.message - ) + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = messageType.message, + isNextButtonEnabled = length < 15, + isDuplicateCheckEnabled = length in 1..14 + )
117-126: 불필요한 괄호 제거 (detekt UnnecessaryParentheses)간단한 스타일 이슈입니다. 빌드 퀄리티 점수 개선을 위해 수정 권장합니다.
[ Suggestion applies to Line 125 ]- val isValid = !exists || (userNickname == nickname) + val isValid = !exists || userNickname == nickname
135-138: 색상 매핑/디자인 스펙 재확인 요청가용일 때 0xFFEA9797(핑크), 불가일 때 0xFFFF0000(레드)입니다. 가용을 긍정 색으로 표시하는지 디자인 확인 바랍니다. 또한 color 리소스로 이전을 권장합니다.
165-173: 닉네임 변경 실패 시 성공 플래그가 유지될 수 있음성공 시 true만 설정하고, 실패/예외 시 false로 명시 초기화가 없습니다. 이전 성공값이 남아 오동작할 수 있습니다.
} else { _duplicateCheckMessageType.value = DuplicateCheckMessageType.UPDATE_FAILED _uiState.value = _uiState.value?.copy( duplicateCheckMessage = DuplicateCheckMessageType.UPDATE_FAILED.message ) + _nicknameChangedSuccess.postValue(false) tokenManager.refreshToken( onSuccess = { changeNickname() }, onFailure = { Timber.e("Failed to refresh token and update nickname") } ) } } catch (e: Exception) { Timber.tag("API Failure").e(e, "Error updating nickname") _duplicateCheckMessageType.value = DuplicateCheckMessageType.SERVER_ERROR _uiState.value = _uiState.value?.copy( duplicateCheckMessage = DuplicateCheckMessageType.SERVER_ERROR.message ) + _nicknameChangedSuccess.postValue(false) }Also applies to: 175-180
169-173: 401 한정 토큰 재발급으로 범위를 축소현재는 상태코드에 관계없이 재발급을 시도합니다. 401에서만 재발급하도록 좁히는 것이 안전합니다.
- tokenManager.refreshToken( + if (response.code() == 401) tokenManager.refreshToken( onSuccess = { changeNickname() }, onFailure = { Timber.e("Failed to refresh token and update nickname") } - ) + )- tokenManager.refreshToken( + if (response.code() == 401) tokenManager.refreshToken( onSuccess = { changeStatus() }, onFailure = { Timber.e("Failed to refresh token and update status") } - ) + )Also applies to: 193-197
25-33: 호환성 LiveData 기본값 보강 및 리소스화 제안
- nickname 기본값을 ""로 두면 바인딩 안정성이 좋아집니다.
- 하드코딩된 문자열/색상 값은 리소스/테마로 이관 권장.
- val nickname: LiveData<String> = MutableLiveData() + val nickname: LiveData<String> = MutableLiveData("")
215-223: ALREADY_IN_USE_SAME 메시지 차별화 제안동일 메시지로 표시되면 “사용 중(타인)”과 “내 현재 닉네임”의 케이스가 구분되지 않아 UX 혼란이 있을 수 있습니다.
- ALREADY_IN_USE("이미 사용 중인 닉네임입니다."), - ALREADY_IN_USE_SAME("이미 사용 중인 닉네임입니다."), + ALREADY_IN_USE("이미 사용 중인 닉네임입니다."), + ALREADY_IN_USE_SAME("현재 사용 중인 닉네임입니다."),또한 이 enum이 온보딩에서도 사용되므로 공용 패키지로 이동하면 의존 방향이 더 명확해집니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (37)
.idea/compiler.xml(1 hunks).idea/gradle.xml(1 hunks)app/src/main/java/com/toyou/toyouandroid/data/emotion/service/EmotionService.kt(2 hunks)app/src/main/java/com/toyou/toyouandroid/data/mypage/service/MypageService.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/AuthService.kt(3 hunks)app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/OnboardingService.kt(2 hunks)app/src/main/java/com/toyou/toyouandroid/di/AppModule.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/di/NetworkModule.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/di/Qualifiers.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/di/RepositoryModule.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/domain/home/repository/HomeRepository.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/domain/profile/repository/ProfileRepository.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/base/MainActivity.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionFragment.kt(6 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionViewModel.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeFragment.kt(7 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeUiState.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeViewModel.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/ModifyFragment.kt(0 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/PreviewFragment.kt(0 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageFragment.kt(6 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageUiState.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageViewModel.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileFragment.kt(5 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileUiState.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileViewModel.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/notice/NoticeFragment.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameFragment.kt(3 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameViewModel.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupStatusFragment.kt(1 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/friend/FriendCardDetailFragment.kt(0 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardContainerFragment.kt(0 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardDetailFragment.kt(0 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/fragment/social/SendFragment.kt(0 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/AuthViewModelFactory.kt(0 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/CardViewModel.kt(2 hunks)app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/HomeViewModelFactory.kt(1 hunks)
💤 Files with no reviewable changes (7)
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/PreviewFragment.kt
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardContainerFragment.kt
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/social/SendFragment.kt
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/friend/FriendCardDetailFragment.kt
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/ModifyFragment.kt
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardDetailFragment.kt
- app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/AuthViewModelFactory.kt
🧰 Additional context used
🧬 Code graph analysis (4)
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeViewModel.kt (2)
app/src/main/java/com/toyou/toyouandroid/utils/calendar/CurrentDate.kt (1)
getCurrentDate(7-10)app/src/main/java/com/toyou/toyouandroid/domain/home/repository/HomeRepository.kt (1)
getYesterdayCard(13-13)
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionViewModel.kt (1)
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionFragment.kt (1)
updateEmotion(152-210)
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameViewModel.kt (1)
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileViewModel.kt (2)
handleNicknameCheckResult(117-143)handleNicknameCheckError(146-153)
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileViewModel.kt (1)
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameViewModel.kt (3)
handleNicknameCheckResult(106-127)handleNicknameCheckError(130-137)checkDuplicate(82-104)
🪛 GitHub Check: CodeFactor
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileViewModel.kt
[warning] 125-125: app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileViewModel.kt#L125
Parentheses in (userNickname == nickname) are unnecessary and can be replaced with: userNickname == nickname. (detekt.UnnecessaryParentheses)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Continuous Deployment
🔇 Additional comments (13)
app/src/main/java/com/toyou/toyouandroid/di/Qualifiers.kt (1)
5-11: Retrofit 분기용 Qualifier 정의 적절
@AuthRetrofit/@NonAuthRetrofit모두 Binary 유지로 충분합니다. OkHttpClient/Interceptor 레벨에서도 동일 Qualifier를 일관 적용하는지 점검 부탁드립니다.app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/CardViewModel.kt (1)
33-34: 직접 의존성 차단 목적의 주석 처리 OK(임시)
HomeRepository제거 방향과 일치합니다. 단, 임시 주석은 빠르게 Hilt 기반 주입/대체 구현으로 전환해 주세요.app/src/main/java/com/toyou/toyouandroid/presentation/fragment/notice/NoticeFragment.kt (1)
149-151: 상세 조회 호출 제거에 따른 화면 의존성 확인 필요
getCardDetail호출을 제거했습니다.modify_fragment에서 필요한 데이터가 인자로 전달되거나 공유 ViewModel로 보장되는지 확인해 주세요. 없으면 화면 진입 후 NPE/빈 화면 위험이 있습니다.app/src/main/java/com/toyou/toyouandroid/di/AppModule.kt (1)
20-22: ApplicationContext 사용 적절성 검증TokenStorage가 Context를 장기 보관하므로 ApplicationContext만 사용하는지 확인 부탁드립니다. Activity/Fragment context 참조 시 메모리 릭 위험이 있습니다.
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionFragment.kt (1)
199-204: 검토 결과 — 지적이 부정확합니다.HomeViewModel.updateHomeEmotion은 emotionText: String으로 정의되어 있어 String 전달(현재 emotionData.homeEmotionDrawable.toString())은 시그니처와 일치합니다. drawable 리소스 ID(Int)를 전달하려면 ViewModel 시그니처를 Int로 변경해야 합니다.
Likely an incorrect or invalid review comment.
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameFragment.kt (2)
40-41: HomeViewModel 스코프 변경 영향 확인 필요homeViewModel을 activityViewModels()에서 viewModels()로 변경하면 화면 간 공유 상태가 끊길 수 있습니다. 의도라면 OK, 아니라면 activity 스코프로 환원하세요.
29-30: Hilt 적용은 적절합니다@androidentrypoint 적용 및 by viewModels() 전환은 방향성에 부합합니다.
DI 그래프 상 SignupNicknameViewModel이 정상 주입되는지 런타임 확인 부탁드립니다.
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileFragment.kt (1)
42-47: Hilt 기반 ViewModel 주입 전환은 적절합니다@androidentrypoint + by viewModels() 사용 및 바인딩 설정이 올바릅니다.
Also applies to: 69-75
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageFragment.kt (2)
30-31: Hilt 적용 및 UI 상태 관찰 로직 양호@androidentrypoint, by viewModels() 전환과 uiState 관찰/닉네임 폴백 처리가 적절합니다.
Also applies to: 47-52, 108-125
190-195: 로그아웃/회원탈퇴 시 SharedPreferences 플래그 재확인signOut에서
isSubscribed를 true로 설정합니다. 의도된 값인지 확인 필요합니다.app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeViewModel.kt (1)
15-23: Hilt 기반 DI와 UI 상태 통합은 적절합니다@HiltViewModel/@Inject 적용과 HomeUiState 단일 소스로의 통합이 깔끔합니다.
Also applies to: 25-34
app/src/main/java/com/toyou/toyouandroid/di/NetworkModule.kt (1)
21-64: 서비스 바인딩 구성은 합리적입니다 — Qualifiers 존재 확인 완료AuthRetrofit, NonAuthRetrofit가 동일 패키지에 선언되어 있습니다: app/src/main/java/com/toyou/toyouandroid/di/Qualifiers.kt (AuthRetrofit — 라인 5, NonAuthRetrofit — 라인 9).
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameViewModel.kt (1)
86-94: 닉네임 중복 API의 인증 요구사항 확인 필요isSuccessful이 false인 경우 토큰 재발급/재시도 로직이 없습니다. 온보딩의 닉네임 체크가 비인증 엔드포인트라면 문제없지만, 401이 나올 가능성이 있다면 최소한 401에 한해 재발급 후 1회 재시도를 고려하세요.
다음 사항을 확인 부탁드립니다:
- checkNickname 엔드포인트가 비인증인지, 인증 필요 시 401 처리 방안을 적용할지.
| <bytecodeTargetLevel target="17"> | ||
| <module name="ToYou.app" target="21" /> | ||
| </bytecodeTargetLevel> |
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.
모듈별 바이트코드 타깃(21)과 프로젝트 타깃(17) 불일치 — 빌드/런타임 불안정 유발 가능성 큼
Gradle/AGP가 JDK 17 기반이라면 IDE(모듈 21)와 Gradle(프로젝트 17) 결과가 엇갈려 classfile 버전 불일치, 테스트 실패, IDE/CI 상이 동작 등이 발생할 수 있습니다. 당장 통일을 권장합니다.
다음 중 하나로 정리하세요(권장 순서):
- 모듈 오버라이드 제거하고 전체를 17로 유지:
- <bytecodeTargetLevel target="17">
- <module name="ToYou.app" target="21" />
- </bytecodeTargetLevel>
+ <bytecodeTargetLevel target="17" />- 전부 21로 올릴 계획이라면 Gradle compileOptions(source/target), Kotlin jvmTarget, JDK/CI 이미지, AGP 호환성까지 모두 21로 정렬 후 반영하십시오.
추가로, .idea 설정은 팀별 환경 편차가 커서 PR 포함을 지양하는 편이 안전합니다.
📝 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.
| <bytecodeTargetLevel target="17"> | |
| <module name="ToYou.app" target="21" /> | |
| </bytecodeTargetLevel> | |
| <bytecodeTargetLevel target="17" /> |
🤖 Prompt for AI Agents
.idea/compiler.xml lines 4-6: the module-level bytecode target is set to 21
while the project/Gradle target is 17, causing classfile version mismatches; fix
by removing the module override so the module inherits the project target (set
module target to 17 or delete the <module name="..."> entry) and ensure .idea
settings are excluded from the PR (add to .gitignore or revert this file);
alternatively, if you intend to move the whole project to Java 21, update Gradle
compileOptions, Kotlin jvmTarget, CI/JDK images and AGP compatibility to 21
consistently across the repo before changing the IDE file and commit.
💡 Verification agent
🧩 Analysis chain
Gradle/AGP/CI 구성과 IDE 설정 정합성 점검 요청
실제 빌드는 Gradle이 결정합니다. 아래 스크립트로 저장소 전반 설정 불일치 여부를 점검해 주세요.
예상: 전 모듈에서 Java 17(또는 21)로 일관돼야 하며, Kotlin jvmTarget도 동일 JVM 버전으로 정렬되어야 합니다.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "[1] Gradle/AGP/Kotlin 설정 스캔"
fd -HIa -t f 'build.gradle*' | while read -r f; do
echo "---- $f"
rg -n -C1 -e 'compileOptions|sourceCompatibility|targetCompatibility|kotlinOptions|jvmTarget|plugins|com.android.application|com.android.library' "$f" || true
done
echo "[2] Gradle Wrapper 버전 확인"
rg -n 'distributionUrl' gradle/wrapper/gradle-wrapper.properties || true
echo "[3] JDK/CI 설정 확인(GitHub Actions 등)"
fd -HIa -t f '.github/workflows/*.yml' | while read -r wf; do
echo "---- $wf"
rg -n -C1 -e 'setup-java|java-version|JAVA_HOME|gradle-version' "$wf" || true
done
echo "[4] Kotlin 및 Java 버전 상수 검색"
rg -n -e 'JavaVersion\.VERSION_|jvmTarget\s*=\s*"[0-9]+' -S || trueLength of output: 1046
IDE(.idea/compiler.xml)와 Gradle 설정의 Java/Kotlin 타겟 불일치 — 즉시 정렬 필요
-
문제 요약: .idea/compiler.xml은 bytecodeTargetLevel=17, 모듈 target=21으로 되어 있으나 Gradle 설정은 Java/Kotlin이 1.8으로 설정되어 있습니다.
- IDE: .idea/compiler.xml (lines 4–6) — /
- Gradle: app/build.gradle.kts (lines 66–72) — sourceCompatibility = JavaVersion.VERSION_1_8, targetCompatibility = JavaVersion.VERSION_1_8, kotlinOptions.jvmTarget = "1.8"
- 기타: 루트 build.gradle.kts → AGP=8.2.2, Kotlin plugin=1.9.22; gradle/wrapper/gradle-wrapper.properties → gradle-8.2
-
조치(필수): Gradle과 IDE를 일관되게 맞추세요. 선택지 — (A) Gradle 및 kotlinOptions를 Java 17(또는 21)에 맞게 업그레이드하고 jvmTarget을 동일하게 설정하거나, (B) .idea/compiler.xml을 1.8로 낮춰 일치시킬 것. 권장: 빌드는 Gradle이 기준이므로 Gradle 설정을 우선 정렬하고 .idea 파일은 필요시 재생성 또는 VCS에서 제외하세요.
🤖 Prompt for AI Agents
.idea/compiler.xml lines 4–6: bytecodeTargetLevel=17 and module target=21
conflict with Gradle's Java/Kotlin target 1.8; align IDE and Gradle targets. Fix
by updating Gradle (preferred): set sourceCompatibility/targetCompatibility and
kotlinOptions.jvmTarget to 17 (or 21) in app/build.gradle.kts to match the IDE,
then regenerate or commit the updated .idea files; OR downgrade
.idea/compiler.xml to 1.8 if you must keep Gradle at 1.8—prefer updating Gradle
to Java 17/21 and removing .idea from VCS or regenerating it to avoid future
drift.
| <component name="GradleSettings"> | ||
| <option name="linkedExternalProjectsSettings"> | ||
| <GradleProjectSettings> | ||
| <option name="testRunner" value="CHOOSE_PER_TEST" /> |
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.
💡 Verification agent
🧩 Analysis chain
IDE 설정 파일 커밋 지양 + CI 영향 점검
IDE별 편차를 줄이려면 .idea 변경은 VCS에서 제외하는 것을 권장합니다. testRunner=CHOOSE_PER_TEST 변경이 CI/Gradle 테스트 실행에 영향 없는지 확인 부탁드립니다.
.idea가 무시되는지 확인 스크립트:
🏁 Script executed:
#!/bin/bash
rg -nP '\.idea/?$' .gitignore || echo "`.idea/`가 .gitignore에 없음"Length of output: 58
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# check .gitignore
if [ ! -f .gitignore ]; then
echo ".gitignore 파일이 존재하지 않습니다"
else
pattern='^\s*\.idea(/)?\s*$'
if command -v rg >/dev/null 2>&1; then
if rg -nP "$pattern" .gitignore >/dev/null 2>&1; then
echo ".idea/가 .gitignore에 있음"
else
echo ".idea/가 .gitignore에 없음"
fi
else
if grep -nE "$pattern" .gitignore >/dev/null 2>&1; then
echo ".idea/가 .gitignore에 있음"
else
echo ".idea/가 .gitignore에 없음"
fi
fi
fi
# check .idea directory presence
if [ -d .idea ]; then
echo ".idea 디렉터리 존재: 예"
else
echo ".idea 디렉터리 존재: 아니오"
fi
# check tracked files under .idea
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
tracked_files=$(git ls-files -- .idea 2>/dev/null || true)
if [ -n "$tracked_files" ]; then
echo ".idea 아래 버전관리 대상 파일이 발견됨:"
printf '%s\n' "$tracked_files"
else
echo ".idea 아래 버전관리 대상 파일 없음"
fi
else
echo "git 저장소가 아님 또는 git 접근 불가"
fiLength of output: 289
.idea가 버전관리 중 — 즉시 제거 및 CI 영향 확인 필요
- 상태: .gitignore에 ".idea/" 항목 없음. 다음 파일들이 트래킹되어 있음: .idea/.gitignore, .idea/appInsightsSettings.xml, .idea/compiler.xml, .idea/deploymentTargetDropDown.xml, .idea/deploymentTargetSelector.xml, .idea/discord.xml, .idea/gradle.xml, .idea/kotlinc.xml, .idea/migrations.xml, .idea/vcs.xml
- 조치(즉시): 1) .gitignore에 ".idea/" 추가. 2) 이미 트래킹된 파일 제거: git rm -r --cached .idea && git commit -m "Remove IDE settings". 3) .idea/gradle.xml의 변경(testRunner="CHOOSE_PER_TEST")이 CI/Gradle 테스트 실행에 영향이 없는지 CI 파이프라인(또는 로컬에서 gradle test)으로 검증 — 영향이 있으면 리포지토리 변경을 되돌리고 로컬 설정으로 유지.
🤖 Prompt for AI Agents
.idea/gradle.xml (line 7): the repo is tracking .idea IDE settings and a change
to testRunner (testRunner="CHOOSE_PER_TEST") was committed; immediately add
".idea/" to .gitignore, remove tracked IDE files with git rm -r --cached .idea
and commit (e.g., git commit -m "Remove IDE settings"), then validate the gradle
test behavior/CI by running the CI pipeline or locally executing gradle test to
ensure the testRunner change has no effect — if it does affect CI, revert the
repository change and keep the setting locally.
| object NetworkModule { | ||
|
|
||
| @Provides | ||
| @Singleton | ||
| @AuthRetrofit | ||
| fun provideRetrofit(): Retrofit { | ||
| return AuthNetworkModule.getClient() | ||
| } | ||
|
|
||
| @Provides | ||
| @Singleton | ||
| @NonAuthRetrofit | ||
| fun provideNonAuthRetrofit(): Retrofit { | ||
| return NetworkModule.getClient() | ||
| } | ||
|
|
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.
동일 이름 충돌로 인한 컴파일 오류: NetworkModule 참조가 자기 자신으로 바인딩됩니다
DI 모듈 객체명(NetworkModule)과 import된 네트워크 유틸(NetworkModule)이 이름이 동일해, return NetworkModule.getClient()가 현재 파일의 객체 자신을 가리켜 컴파일 에러가 발생합니다. alias로 즉시 수정하세요.
적용 diff:
-import com.toyou.toyouandroid.network.NetworkModule
+import com.toyou.toyouandroid.network.NetworkModule as LegacyNetworkModule
...
- fun provideNonAuthRetrofit(): Retrofit {
- return NetworkModule.getClient()
- }
+ fun provideNonAuthRetrofit(): Retrofit {
+ return LegacyNetworkModule.getClient()
+ }🤖 Prompt for AI Agents
In app/src/main/java/com/toyou/toyouandroid/di/NetworkModule.kt around lines 19
to 34, the DI object NetworkModule name conflicts with an imported network
utility of the same name causing return NetworkModule.getClient() to reference
the object itself; fix by aliasing the imported NetworkModule (e.g., import
...NetworkModule as NetworkUtil) or use the utility's fully-qualified name and
then change provideNonAuthRetrofit() to call the aliased/fully-qualified
getClient() (e.g., NetworkUtil.getClient()) so the DI object no longer
self-references.
| suspend fun updateStatus(status: String): Response<PatchNicknameResponse> { | ||
| return onboardingService.patchStatusSuspend(PatchStatusRequest(status)) | ||
| } |
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.
반환 타입 오류: 상태 수정 API가 닉네임 응답 타입을 반환
updateStatus가 Response<PatchNicknameResponse>를 반환하고 있어 타입 불일치로 이어집니다. PatchStatusResponse로 교체하세요.
-import com.toyou.toyouandroid.data.onboarding.dto.PatchNicknameResponse
+import com.toyou.toyouandroid.data.onboarding.dto.PatchNicknameResponse
+import com.toyou.toyouandroid.data.onboarding.dto.PatchStatusResponse
@@
- suspend fun updateStatus(status: String): Response<PatchNicknameResponse> {
+ suspend fun updateStatus(status: String): Response<PatchStatusResponse> {
return onboardingService.patchStatusSuspend(PatchStatusRequest(status))
}📝 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.
| suspend fun updateStatus(status: String): Response<PatchNicknameResponse> { | |
| return onboardingService.patchStatusSuspend(PatchStatusRequest(status)) | |
| } | |
| suspend fun updateStatus(status: String): Response<PatchStatusResponse> { | |
| return onboardingService.patchStatusSuspend(PatchStatusRequest(status)) | |
| } |
🤖 Prompt for AI Agents
In
app/src/main/java/com/toyou/toyouandroid/domain/profile/repository/ProfileRepository.kt
around lines 21 to 23, the method updateStatus is declared to return
Response<PatchNicknameResponse> but the status API returns PatchStatusResponse;
change the return type to Response<PatchStatusResponse>, update any related
imports/usages, and ensure the onboardingService.patchStatusSuspend call matches
the new response type (adjust service interface if necessary).
| data class HomeUiState( | ||
| val currentDate: String = "", | ||
| val emotionText: String = "멘트", | ||
| val diaryCards: List<DiaryCard>? = null, | ||
| val yesterdayCards: List<YesterdayCard> = emptyList(), | ||
| val isLoading: Boolean = false, | ||
| val isEmpty: Boolean = false | ||
| ) No newline at end of file |
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.
💡 Verification agent
🧩 Analysis chain
UI 상태 모델에서 null과 중복 상태(isEmpty) 제거 제안
diaryCards를 nullable로 두고(List<DiaryCard>?) 별도의isEmpty플래그를 유지하면 상태가 불일치할 수 있습니다. 비어있음을 리스트의 크기에서 유도하도록 통일하는 편이 안전합니다.- 제안:
diaryCards를emptyList()기본값의 non-null로 바꾸고isEmpty필드는 제거하세요. 사용처는uiState.diaryCards.isEmpty()로 판단하면 됩니다.
다음 스크립트로 isEmpty 사용 여부를 확인해 주세요.
적용 예시:
data class HomeUiState(
val currentDate: String = "",
- val emotionText: String = "멘트",
- val diaryCards: List<DiaryCard>? = null,
+ val emotionText: String = "",
+ val diaryCards: List<DiaryCard> = emptyList(),
val yesterdayCards: List<YesterdayCard> = emptyList(),
- val isLoading: Boolean = false,
- val isEmpty: Boolean = false
+ val isLoading: Boolean = false
)🏁 Script executed:
#!/bin/bash
rg -nP --type=kt '\bHomeUiState\b' -C2
rg -nP --type=kt '\bisEmpty\b' -C2Length of output: 53
rg --type=kt 오류로 검색이 실패했습니다. 아래 스크립트로 HomeUiState 정의와 isEmpty/diaryCards 사용처를 재검증하겠습니다.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Searching for data class HomeUiState definition ==="
rg -nP '\bdata\s+class\s+HomeUiState\b' -S -n -C2 || true
echo "=== All occurrences of HomeUiState ==="
rg -nP '\bHomeUiState\b' -S -n -C2 || true
echo "=== Searching for 'val isEmpty' property definitions ==="
rg -nP '\bval\s+isEmpty\b' -S -n -C2 || true
echo "=== Searching for 'diaryCards' occurrences ==="
rg -nP '\bdiaryCards\b' -S -n -C3 || true
echo "=== Looking for HomeUiState.kt file(s) and printing head ==="
find . -type f -name 'HomeUiState.kt' -print -exec sed -n '1,200p' {} \; || true
echo "=== Done ==="Length of output: 9130
HomeUiState: diaryCards를 non-null(emptyList())으로 변경하고 isEmpty 필드 제거 — 관련 코드 동시 수정 필요
- 검증: isEmpty는 HomeUiState에만 정의되어 있어 제거해도 무방합니다 (app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeUiState.kt).
- 조치: HomeUiState에서 diaryCards를 nullable -> List = emptyList()로 변경하고 isEmpty 필드를 제거하세요. 동시에 app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeViewModel.kt의 resetState()에서 diaryCards = null을 diaryCards = emptyList()로 교체하고, diaryCards에 null을 할당하는 모든 위치도 동일하게 수정해야 합니다.
🤖 Prompt for AI Agents
In
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeUiState.kt
around lines 6-13, change diaryCards from nullable List<DiaryCard>? to a
non-nullable List<DiaryCard> = emptyList() and remove the isEmpty property; then
update
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeViewModel.kt
(notably resetState()) to set diaryCards = emptyList() instead of null and
search/replace any other assignments that set diaryCards = null to diaryCards =
emptyList() so all code paths use a non-null empty list.
| fun kakaoLogout() { | ||
| viewModelScope.launch { | ||
| val refreshToken = tokenStorage.getRefreshToken().toString() | ||
| val accessToken = tokenStorage.getAccessToken().toString() | ||
| Timber.d("Attempting to logout in with refresh token: $refreshToken") | ||
| Timber.d("Attempting to logout with refresh token: $refreshToken") | ||
| Timber.d("accessToken: $accessToken") | ||
|
|
||
| authService.logout(refreshToken).enqueue(object : Callback<SignUpResponse> { | ||
| override fun onResponse(call: Call<SignUpResponse>, response: Response<SignUpResponse>) { | ||
| if (response.isSuccessful) { | ||
| Timber.i("Logout successfully") | ||
| _logoutSuccess.value = true | ||
| tokenStorage.clearTokens() | ||
| try { | ||
| val response = authService.logoutSuspend(refreshToken) | ||
| if (response.isSuccessful) { | ||
| Timber.i("Logout successfully") | ||
| _logoutSuccess.value = true | ||
| tokenStorage.clearTokens() | ||
| } else { | ||
| val errorMessage = response.errorBody()?.string() ?: "Unknown error: ${response.message()}" | ||
| Timber.e("API Error: $errorMessage") | ||
|
|
||
| if (response.code() == 401) { | ||
| tokenManager.refreshToken( | ||
| onSuccess = { kakaoLogout() }, | ||
| onFailure = { | ||
| Timber.e("Failed to refresh token and kakao logout") | ||
| _logoutSuccess.value = false | ||
| } | ||
| ) | ||
| } else { | ||
| val errorMessage = response.errorBody()?.string() ?: "Unknown error: ${response.message()}" | ||
| Timber.e("API Error: $errorMessage") | ||
| _logoutSuccess.value = false | ||
|
|
||
| // 토큰 만료 시 토큰 갱신 후 로그아웃 재시도 | ||
| if (response.code() == 401) { | ||
| tokenManager.refreshToken( | ||
| onSuccess = { kakaoLogout() }, // 토큰 갱신 후 로그아웃 재시도 | ||
| onFailure = { Timber.e("Failed to refresh token and kakao logout") } | ||
| ) | ||
| } else { | ||
| _logoutSuccess.value = false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun onFailure(call: Call<SignUpResponse>, t: Throwable) { | ||
| val errorMessage = t.message ?: "Unknown error" | ||
| Timber.e("Network Failure: $errorMessage") | ||
| _logoutSuccess.value = false | ||
| } | ||
| }) | ||
| } catch (e: Exception) { | ||
| Timber.e("Network Failure: ${e.message}") | ||
| _logoutSuccess.value = false | ||
| } | ||
| } |
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.
💡 Verification agent
🧩 Analysis chain
토큰 관련 로그/사용처 전수 점검 스크립트 제공
레포 전역에서 토큰을 로그로 노출하는 부분이 더 없는지 점검 필요합니다.
🏁 Script executed:
#!/bin/bash
# 레포 전역에서 토큰 취득/로그 출력 패턴 점검
rg -n -C2 -e 'getAccessToken|getRefreshToken' -e 'Timber\.(d|i|w|e)\('Length of output: 90435
긴급: 액세스/리프레시/FCM 토큰 평문 로그 즉시 제거
스크립트 결과 레포 전역에서 민감 토큰(Access / Refresh / FCM)이 평문으로 로깅되고 있습니다 — 즉시 제거 또는 마스킹 필요.
- app/src/main/java/com/toyou/toyouandroid/utils/TokenManager.kt : line ~20 — Timber.d("Tokens received from server - Access: $newAccessToken, Refresh: $newRefreshToken") → 즉시 삭제(최우선).
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageViewModel.kt : lines ~43-46, ~79-82 — kakaoLogout / kakaoSignOut에서 refresh/access 토큰 평문 출력 → 제거/마스킹.
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/LoginViewModel.kt : lines ~59-66, 73, 121, 159-170 — checkIfTokenExists / kakaoLogin / signUp / reissueJWT 등에서 토큰 평문 출력 → 제거/마스킹.
- app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupStatusFragment.kt : line ~119 — accessToken 포함 로그 → 제거.
- app/src/main/java/com/toyou/toyouandroid/fcm/MyFirebaseMessagingService.kt : lines ~35, ~37 — FCM 토큰 평문 로깅 → 제거 또는 안전 저장(로그 금지).
- 그 외 여러 ViewModel/Fragment(예: ProfileViewModel 등)에서도 tokenStorage.getAccessToken()/getRefreshToken()와 Timber 로그 조합 확인됨 — 레포 전체에서 민감 토큰 로그 전수 제거 필요.
권장 조치(간단): 토큰 내용 로그 전면 제거 또는 마스킹(예: 마지막 4자리만), 릴리즈 빌드에서 민감 로그 비활성화, TokenManager.kt 로그 우선 삭제 및 재배포.
| fun kakaoLogout() { | ||
| viewModelScope.launch { | ||
| val refreshToken = tokenStorage.getRefreshToken().toString() | ||
| val accessToken = tokenStorage.getAccessToken().toString() | ||
| Timber.d("Attempting to logout in with refresh token: $refreshToken") | ||
| Timber.d("Attempting to logout with refresh token: $refreshToken") | ||
| Timber.d("accessToken: $accessToken") | ||
|
|
||
| authService.logout(refreshToken).enqueue(object : Callback<SignUpResponse> { | ||
| override fun onResponse(call: Call<SignUpResponse>, response: Response<SignUpResponse>) { | ||
| if (response.isSuccessful) { | ||
| Timber.i("Logout successfully") | ||
| _logoutSuccess.value = true | ||
| tokenStorage.clearTokens() | ||
| try { | ||
| val response = authService.logoutSuspend(refreshToken) | ||
| if (response.isSuccessful) { | ||
| Timber.i("Logout successfully") | ||
| _logoutSuccess.value = true | ||
| tokenStorage.clearTokens() | ||
| } else { | ||
| val errorMessage = response.errorBody()?.string() ?: "Unknown error: ${response.message()}" |
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.
null 토큰 처리 부재로 "null" 문자열 전송 가능
toString() 사용 시 null이면 "null"이 서버로 전달될 수 있습니다. 명시적으로 null 체크 후 실패 처리/조기 반환하세요.
- val response = authService.logoutSuspend(refreshToken)
+ val token = refreshToken ?: run {
+ Timber.w("No refresh token present; logout aborted")
+ _logoutSuccess.value = false
+ return@launch
+ }
+ val response = authService.logoutSuspend(token)동일 패턴을 kakaoSignOut에도 적용하세요.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageViewModel.kt
around lines 41 to 55, you call toString() on potentially null tokens which will
send the literal "null" to the server; instead, check
tokenStorage.getRefreshToken() and getAccessToken() for null/empty before
proceeding and return or set an error state if missing (do not call toString()),
and only use the token when non-null; apply the same explicit null/empty guard
and early return/error handling to the kakaoSignOut flow as well.
| val refreshToken = tokenStorage.getRefreshToken().toString() | ||
| val accessToken = tokenStorage.getAccessToken().toString() | ||
| Timber.d("Attempting to logout in with refresh token: $refreshToken") | ||
| Timber.d("Attempting to logout with refresh token: $refreshToken") | ||
| Timber.d("accessToken: $accessToken") | ||
|
|
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.
민감정보(토큰) 로그 유출 위험: refresh/access token을 Timber로 출력
토큰은 비밀정보입니다. 로그 유출은 계정 탈취로 직결될 수 있습니다. 즉시 제거/마스킹이 필요합니다. 또한 accessToken 변수를 읽지만 사용하지 않습니다.
- val refreshToken = tokenStorage.getRefreshToken().toString()
- val accessToken = tokenStorage.getAccessToken().toString()
- Timber.d("Attempting to logout with refresh token: $refreshToken")
- Timber.d("accessToken: $accessToken")
+ val refreshToken = tokenStorage.getRefreshToken()
+ // Do not log tokens- val refreshToken = tokenStorage.getRefreshToken().toString()
- val accessToken = tokenStorage.getAccessToken().toString()
- Timber.d("Attempting to signout with refresh token: $refreshToken")
- Timber.d("accessToken: $accessToken")
+ val refreshToken = tokenStorage.getRefreshToken()
+ // Do not log tokens추가로 null 안전 처리도 함께 적용하세요(아래 코멘트 참고).
Also applies to: 79-83
| if (response.code() == 401) { | ||
| tokenManager.refreshToken( | ||
| onSuccess = { kakaoLogout() }, | ||
| onFailure = { | ||
| Timber.e("Failed to refresh token and kakao logout") | ||
| _logoutSuccess.value = false | ||
| } | ||
| ) | ||
| } else { |
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.
🛠️ Refactor suggestion
401 한정 재발급/재시도로 한 번만 재시도하도록 가드 추가 필요
- kakaoLogout: 401일 때 무제한 재귀적 재시도가 가능합니다. 재발급 성공 후에도 계속 401이 나면 반복됩니다.
- kakaoSignOut/updateMypage: 현재는 상태코드에 상관없이(혹은 무조건) 토큰 재발급을 시도합니다.
간단한 재시도 가드(retry 플래그)와 401 한정 처리 제안:
- fun kakaoLogout() {
+ fun kakaoLogout(retry: Boolean = true) {
...
- } else {
+ } else {
val errorMessage = response.errorBody()?.string() ?: "Unknown error: ${response.message()}"
Timber.e("API Error: $errorMessage")
- if (response.code() == 401) {
+ if (response.code() == 401 && retry) {
tokenManager.refreshToken(
- onSuccess = { kakaoLogout() },
+ onSuccess = { kakaoLogout(retry = false) },
onFailure = {
Timber.e("Failed to refresh token and kakao logout")
_logoutSuccess.value = false
}
)
} else {
_logoutSuccess.value = false
}
}- val response = authService.signOutSuspend(refreshToken)
+ val token = refreshToken ?: run {
+ Timber.w("No refresh token present; signout aborted")
+ _signOutSuccess.value = false
+ return@launch
+ }
+ val response = authService.signOutSuspend(token)
if (response.isSuccessful) {
...
- } else {
+ } else {
val errorMessage = response.errorBody()?.string() ?: "Unknown error"
Timber.e("API Error: $errorMessage")
- tokenManager.refreshToken(
- onSuccess = { kakaoSignOut() },
- onFailure = {
- Timber.e("Failed to refresh token and kakao signout")
- _signOutSuccess.value = false
- }
- )
+ if (response.code() == 401) {
+ tokenManager.refreshToken(
+ onSuccess = { kakaoSignOut() },
+ onFailure = {
+ Timber.e("Failed to refresh token and kakao signout")
+ _signOutSuccess.value = false
+ }
+ )
+ } else {
+ _signOutSuccess.value = false
+ }
}- } else {
+ } else {
Timber.tag("API Error").e("Failed to update Mypage. Code: ${response.code()}, Message: ${response.message()}")
_uiState.value = _uiState.value?.copy(isLoading = false)
- tokenManager.refreshToken(
- onSuccess = { updateMypage() },
- onFailure = {
- Timber.e("Failed to refresh token and get mypage")
- _uiState.value = _uiState.value?.copy(isLoading = false)
- }
- )
+ if (response.code() == 401) {
+ tokenManager.refreshToken(
+ onSuccess = { updateMypage() },
+ onFailure = {
+ Timber.e("Failed to refresh token and get mypage")
+ _uiState.value = _uiState.value?.copy(isLoading = false)
+ }
+ )
+ }
}Also applies to: 93-100, 127-133
🤖 Prompt for AI Agents
In
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageViewModel.kt
around lines 58-66 (also apply same change at 93-100 and 127-133): the current
flow can trigger unlimited recursive token refresh + retry on 401 (and some
flows trigger refresh regardless of status); add a local retry guard (e.g.,
Boolean flag or request-scoped retry parameter) that is checked before
attempting tokenManager.refreshToken, only perform refresh-and-retry when
response.code() == 401 and retry flag is false, set the flag to true before
calling refresh so a subsequent retry won’t trigger another refresh, and ensure
other code paths (kakaoSignOut/updateMypage) only attempt refresh when status ==
401 and respect the same retry guard; reset or scope the flag appropriately so
it doesn’t persist across independent user-initiated requests.
| private val viewModel: SignupNicknameViewModel by viewModels() | ||
| private val nicknameViewModel: SignupNicknameViewModel by viewModels() | ||
|
|
||
| private lateinit var homeViewModel: HomeViewModel | ||
| private val homeViewModel: HomeViewModel by viewModels() | ||
| private lateinit var viewModelManager: ViewModelManager |
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.
동일 타입 ViewModel 중복 생성으로 인한 상태 분산 위험
SignupNicknameViewModel를 fragment 스코프로 2개(viewModel, nicknameViewModel) 생성합니다. 바인딩은 viewModel을, ViewModelManager는 nicknameViewModel을 사용해 상태가 분산/불일치할 수 있습니다. 하나로 통일하세요.
다음과 같이 정리하는 것을 권장합니다:
- private val viewModel: SignupNicknameViewModel by viewModels()
- private val nicknameViewModel: SignupNicknameViewModel by viewModels()
+ private val viewModel: SignupNicknameViewModel by viewModels()
...
- viewModelManager = ViewModelManager(nicknameViewModel, homeViewModel)
+ viewModelManager = ViewModelManager(viewModel, homeViewModel)Also applies to: 61-61
🤖 Prompt for AI Agents
In
app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameFragment.kt
around lines 37-41 (and also line 61), two instances of SignupNicknameViewModel
are being created (viewModel and nicknameViewModel) which causes dispersed
state; consolidate to a single SignupNicknameViewModel instance: remove one of
the declarations and update all usages (binding and ViewModelManager) to
reference the same viewModel variable so both the UI binding and
ViewModelManager operate on the identical view model instance.


#️⃣연관된 이슈
📝작업 내용
마이페이지부터 리팩토링하기 시작했습니다.
우선, Factory 패턴이 전역에 적용되어있어서 일부 ViewModelFactory를 주석처리했고 Fragment에서 사용하던 ViewModelFactory 또한 임시로 주석처리해두었습니다.
그리고 예시를 위해서 Application, Activity, 일부 ViewModel, Repository, Fragment에 Hilt를 적용해두었습니다. 기존에 network 패키지에 싱글톤으로 사용하던 module들은 전역에서 사용하고 있으므로 점진적인 마이그레이션을 위해서 di 패키지를 생성해서 Hilt를 적용한 module들을 추가했습니다.
보일러플레이트코드라기보다는 체인처럼 맞물려있어서 viewmodel 하나를 수정하면 그에 따른 파일들을 모두 수정해야해서 변경된 파일들의 수는 많지만 대부분 임시 주석 처리를 해둔 점 알고 계시면 좋을 것 같습니다.
그렇기 때문에 한 번에 모든 것을 바꾸지는 못하기에 우선 마이페이지 화면부터 기능 테스트를 진행 완료했습니다.
처음부터 안드로이드 진영에서의 클린 아키텍처와 compose를 사용했더라면 이라는 생각도 하게 되네요...
그래도 코드 가독성, 테스트 코드 작성 시간이나 새 기능을 추가할 때 훨씬 편해지는 것을 기대하고 차근차근 해보는 것이 좋을 것 같습니다.
💬리뷰 요구사항(선택)
특이사항이 있다면 기존 network 패키지에 있던 모듈을 바탕으로 새롭게 di 패키지에 hilt가 적용되어있는 모듈을 생성하다보니 싱글톤으로 구현되는 Hilt 특성상 Retrofit이 중복 바인딩이 되는 상황이 발생해서 충돌이 발생했습니다.
그래서 임시로 Qualifier 어노테이션을 생성해서 인증이 필요한 서비스, 인증이 필요하지 않은 서비스를 구분해두어서 임시로 충돌을 피했습니다.
@Provides @Singleton @AuthRetrofit fun provideRetrofit(): Retrofit { return AuthNetworkModule.getClient() } @Provides @Singleton @NonAuthRetrofit fun provideNonAuthRetrofit(): Retrofit { return NetworkModule.getClient() }commit이 많아서 Android Studio 미어캣 버전에 수록된 기능을 사용했습니다. 그래서 Gemini가 commit 메시지를 작성해주는 바람에 영어로 작성된 점은 안타깝네요.
commit 중에서 아래 메시지로 되어있는 부분이 마이페이지 관련된 내용이므로 해당 부분을 집중적으로 확인해주시면 됩니다.
[Refactor: Apply Hilt and coroutines to Mypage]
Summary by CodeRabbit