Skip to content

Conversation

@tristanjung1006
Copy link
Contributor

@tristanjung1006 tristanjung1006 commented Sep 15, 2025

#️⃣연관된 이슈

#117

📝작업 내용

마이페이지부터 리팩토링하기 시작했습니다.

우선, Factory 패턴이 전역에 적용되어있어서 일부 ViewModelFactory를 주석처리했고 Fragment에서 사용하던 ViewModelFactory 또한 임시로 주석처리해두었습니다.

그리고 예시를 위해서 Application, Activity, 일부 ViewModel, Repository, Fragment에 Hilt를 적용해두었습니다. 기존에 network 패키지에 싱글톤으로 사용하던 module들은 전역에서 사용하고 있으므로 점진적인 마이그레이션을 위해서 di 패키지를 생성해서 Hilt를 적용한 module들을 추가했습니다.

보일러플레이트코드라기보다는 체인처럼 맞물려있어서 viewmodel 하나를 수정하면 그에 따른 파일들을 모두 수정해야해서 변경된 파일들의 수는 많지만 대부분 임시 주석 처리를 해둔 점 알고 계시면 좋을 것 같습니다.

그렇기 때문에 한 번에 모든 것을 바꾸지는 못하기에 우선 마이페이지 화면부터 기능 테스트를 진행 완료했습니다.

처음부터 안드로이드 진영에서의 클린 아키텍처와 compose를 사용했더라면 이라는 생각도 하게 되네요...
그래도 코드 가독성, 테스트 코드 작성 시간이나 새 기능을 추가할 때 훨씬 편해지는 것을 기대하고 차근차근 해보는 것이 좋을 것 같습니다.

💬리뷰 요구사항(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

package com.toyou.toyouandroid.di

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthRetrofit

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class NonAuthRetrofit

특이사항이 있다면 기존 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

  • New Features
    • 프로필 닉네임/상태 편집 UX 개선: 중복 확인 메시지·버튼 상태 표시 강화
    • 마이페이지에 닉네임·친구 수 표기 및 로딩/빈 상태 표시
    • 홈 화면 로딩/빈 상태 표시 및 어제 카드 불러오기 경험 개선
  • Bug Fixes
    • 인증 만료(401) 시 자동 토큰 갱신·재시도로 로그아웃/회원탈퇴/마이페이지 갱신 실패 완화
    • 네트워크 예외 처리 및 오류 메시지 강화로 안정성 향상
  • Refactor
    • 전역 Hilt DI·코루틴 기반 API로 마이그레이션해 초기화 단순화와 성능 개선
  • Chores
    • 사용되지 않는 팩토리/임포트 정리

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` 함수 추가
@tristanjung1006 tristanjung1006 self-assigned this Sep 15, 2025
@tristanjung1006 tristanjung1006 added the ♻️ refactor 코드 개선 label Sep 15, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 15, 2025

Walkthrough

프로젝트 전반에 Hilt DI를 도입하고 Retrofit 호출을 suspend 기반으로 이중화했습니다. Home/Mypage/Profile 영역은 UI 상태(data class) 중심으로 ViewModel을 재구성했고, 다수의 Fragment와 Activity에 @androidentrypoint를 적용했습니다. 기존 ViewModelFactory는 제거/비활성화되었으며, 네트워크 모듈과 Qualifier를 추가했습니다.

Changes

Cohort / File(s) Change summary
IDE 설정
\.idea/compiler.xml, \.idea/gradle.xml
모듈별 바이트코드 타깃 추가(ToYou.app=21), Gradle testRunner 옵션 추가 및 불필요 옵션 제거.
Retrofit 서비스 suspend 이중화
app/src/main/java/.../emotion/service/EmotionService.kt, .../mypage/service/MypageService.kt, .../onboarding/service/AuthService.kt, .../onboarding/service/OnboardingService.kt
기존 Call<T> 유지하며 동일 엔드포인트의 suspend Response<T> 메서드 추가.
DI 인프라(Hilt/Retrofit)
app/src/main/java/.../di/Qualifiers.kt, .../di/NetworkModule.kt, .../di/AppModule.kt, .../di/RepositoryModule.kt
@qualifier 추가(AuthRetrofit/NonAuthRetrofit), 인증/비인증 Retrofit 제공자 및 서비스 바인딩, TokenStorage/TokenManager/Repository 제공자 추가.
리포지토리 구성
app/src/main/java/.../domain/home/repository/HomeRepository.kt, .../domain/profile/repository/ProfileRepository.kt
HomeRepository를 서비스 주입 기반으로 변경(@Inject), ProfileRepository 신규 추가(OnboardingService 위임).
Home 기능 리팩터링
app/src/main/java/.../presentation/fragment/home/HomeUiState.kt, .../HomeViewModel.kt, .../HomeFragment.kt, .../fragment/emotionstamp/HomeOptionViewModel.kt, .../emotionstamp/HomeOptionFragment.kt, .../home/ModifyFragment.kt, .../home/PreviewFragment.kt
Hilt 도입(@HiltViewModel/@androidentrypoint), 단일 HomeUiState로 상태 통합, suspend 호출/토큰 리프레시 처리, Fragment들에서 viewModels()로 전환, 일부 이전 로직/옵저버 주석 처리 및 불필요 import 제거.
Mypage 기능 리팩터링
app/src/main/java/.../presentation/fragment/mypage/MypageUiState.kt, .../MypageViewModel.kt, .../MypageFragment.kt
Hilt 주입, UI 상태(MypageUiState) 도입, suspend 기반 logout/signout/mypage 조회 및 401 시 토큰 갱신 후 재시도, Fragment DI 전환과 바인딩 패턴 적용.
Profile 기능 리팩터링
app/src/main/java/.../presentation/fragment/mypage/ProfileUiState.kt, .../ProfileViewModel.kt, .../ProfileFragment.kt
Hilt 주입, ProfileUiState/StatusType 추가, 닉네임/상태 처리 로직을 Repository+suspend로 이관, UI 바인딩용 LiveData 노출 재구성, Fragment DI 전환 및 옵저버/정리 코드 축소.
온보딩 닉네임 화면
app/src/main/java/.../onboarding/SignupNicknameViewModel.kt, .../onboarding/SignupNicknameFragment.kt
ViewModel을 Hilt 주입으로 전환, ProfileRepository 사용해 닉네임 중복 확인을 suspend로 수행, UI 상태 및 메시지 타입 기반 처리, Fragment에서 activityViewModels() → viewModels() 전환.
경미한 정리/네비게이션
app/src/main/java/.../notice/NoticeFragment.kt, .../record/friend/FriendCardDetailFragment.kt, .../record/my/MyCardContainerFragment.kt, .../record/my/MyCardDetailFragment.kt, .../social/SendFragment.kt, .../onboarding/SignupStatusFragment.kt
불필요 import 제거 및 주석화, 특정 데이터 로드 호출(card detail) 비활성화, 기능 변화 없음.
Activity 엔트리포인트
app/src/main/java/.../presentation/base/MainActivity.kt
@androidentrypoint 추가.
ViewModel 팩토리 제거
app/src/main/java/.../presentation/viewmodel/AuthViewModelFactory.kt, .../presentation/viewmodel/HomeViewModelFactory.kt
AuthViewModelFactory 삭제, HomeViewModelFactory 전면 주석 처리로 비활성화.
CardViewModel 변경
app/src/main/java/.../presentation/viewmodel/CardViewModel.kt
HomeRepository 의존 주석 처리, getCardDetail 메서드 전체 주석 처리(비활성화).

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Release/1.1.0 #111: HomeRepository/getYesterdayCard와 HomeViewModel/HomeFragment 변경 등 홈 흐름과 데이터 취득 로직을 편집한 PR로, 본 PR의 Home 영역 리팩터링 및 suspend/DI 전환과 직접적으로 맞물립니다.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/#117

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.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

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.

❤️ Share

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

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.32% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed PR 설명은 템플릿의 필수 섹션(연관된 이슈, 작업 내용)을 충실히 포함하고 있으며 리팩토링 범위, 점진적 DI(디렉토리/모듈 구조), 마이페이지에서의 테스트 완료 등 리뷰어가 알아야 할 주요 정보를 상세히 적고 있습니다. 또한 Qualifier 도입 등의 특이사항과 리뷰에서 중점적으로 볼 부분을 명시해 두어 검토에 필요한 컨텍스트가 충분합니다. 스크린샷은 없지만 이는 선택 항목이므로 전반적으로 설명은 요구사항을 충족합니다.
Title Check ✅ Passed 제목 "Refactor/#117 mypage"는 PR의 핵심인 MyPage 리팩터링을 직접적으로 언급하여 변경사항과 목적에 부합합니다. 다만 브랜치명/번호 표기("/#117")와 소문자 표기가 포함되어 있어 히스토리 열람 시 불필요한 노이즈가 될 수 있습니다.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
7.5% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (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 = signUpStatusViewModel

Fragment 하단 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 관찰로 중복 옵저버 누적 위험

클릭할 때마다 emotioncardId를 중첩 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.CreateService
app/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 적용 LGTM

Hilt 진입점 지정 적절합니다. 추가로, 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): ProfileRepository
app/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 적용 상태입니다. UserViewModelby 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

📥 Commits

Reviewing files that changed from the base of the PR and between a0748fc and a7fc58f.

📒 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 처리 방안을 적용할지.

Comment on lines +4 to +6
<bytecodeTargetLevel target="17">
<module name="ToYou.app" target="21" />
</bytecodeTargetLevel>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

모듈별 바이트코드 타깃(21)과 프로젝트 타깃(17) 불일치 — 빌드/런타임 불안정 유발 가능성 큼

Gradle/AGP가 JDK 17 기반이라면 IDE(모듈 21)와 Gradle(프로젝트 17) 결과가 엇갈려 classfile 버전 불일치, 테스트 실패, IDE/CI 상이 동작 등이 발생할 수 있습니다. 당장 통일을 권장합니다.

다음 중 하나로 정리하세요(권장 순서):

  1. 모듈 오버라이드 제거하고 전체를 17로 유지:
-    <bytecodeTargetLevel target="17">
-      <module name="ToYou.app" target="21" />
-    </bytecodeTargetLevel>
+    <bytecodeTargetLevel target="17" />
  1. 전부 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.

Suggested change
<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 || true

Length 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" />
Copy link

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 접근 불가"
fi

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

Comment on lines +19 to +34
object NetworkModule {

@Provides
@Singleton
@AuthRetrofit
fun provideRetrofit(): Retrofit {
return AuthNetworkModule.getClient()
}

@Provides
@Singleton
@NonAuthRetrofit
fun provideNonAuthRetrofit(): Retrofit {
return NetworkModule.getClient()
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

동일 이름 충돌로 인한 컴파일 오류: 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.

Comment on lines +21 to +23
suspend fun updateStatus(status: String): Response<PatchNicknameResponse> {
return onboardingService.patchStatusSuspend(PatchStatusRequest(status))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

반환 타입 오류: 상태 수정 API가 닉네임 응답 타입을 반환

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

Suggested change
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).

Comment on lines +6 to +13
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
Copy link

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 플래그를 유지하면 상태가 불일치할 수 있습니다. 비어있음을 리스트의 크기에서 유도하도록 통일하는 편이 안전합니다.
  • 제안: diaryCardsemptyList() 기본값의 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' -C2

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

Comment on lines 41 to 74
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
}
}
Copy link

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 로그 우선 삭제 및 재배포.

Comment on lines 41 to +55
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()}"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Comment on lines 43 to 47
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")

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

민감정보(토큰) 로그 유출 위험: 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

Comment on lines +58 to 66
if (response.code() == 401) {
tokenManager.refreshToken(
onSuccess = { kakaoLogout() },
onFailure = {
Timber.e("Failed to refresh token and kakao logout")
_logoutSuccess.value = false
}
)
} else {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

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.

Comment on lines +37 to 41
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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

동일 타입 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.

@tristanjung1006 tristanjung1006 changed the title Refactor/#117 m Refactor/#117 mypage Sep 15, 2025
@tristanjung1006 tristanjung1006 merged commit abdb1d6 into master Dec 28, 2025
4 of 5 checks passed
@tristanjung1006 tristanjung1006 deleted the refactor/#117 branch December 28, 2025 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

♻️ refactor 코드 개선

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants