Skip to content

[Feat/#157] 프로필 추가 로직 구현#163

Merged
nahy-512 merged 21 commits into
developfrom
feat/#157-profile-api-detail
Jan 22, 2026
Merged

[Feat/#157] 프로필 추가 로직 구현#163
nahy-512 merged 21 commits into
developfrom
feat/#157-profile-api-detail

Conversation

@nahy-512

@nahy-512 nahy-512 commented Jan 22, 2026

Copy link
Copy Markdown
Contributor

📮 관련 이슈

📌 작업 내용

  • userId 바탕 내 프로필/타인 프로필 화면이동 및 API 호출 구분
  • 저장한 작품 OTT 바텀시트 추가
  • 토큰 저장 로직 개선

📸 스크린샷

프로필 이동 북마크 낙관적 UI
Screen_recording_20260122_163120.mp4
Screen_recording_20260122_175248.mp4

🫛 To. 리뷰어

  • 내 프로필은 비공개 컬렉션도 다 보여줘야 해서.. API를 다른 걸 써야하네요

Summary by CodeRabbit

릴리스 노트

  • 새 기능

    • 프로필 화면에서 콘텐츠 클릭 시 OTT 리스트 하단 팝업 표시 추가
    • 사용자별 북마크된 콘텐츠 조회 기능 추가
    • 컬렉션 목록에서 다른 사용자의 생성/북마크 컬렉션 조회 지원
  • 개선 사항

    • 인증 토큰·유저 ID 저장 흐름 개선
    • 컬렉션 북마크 토글에 낙관적 UI와 디바운스(롤백 지원) 도입
    • 내비게이션 및 화면 간 파라미터 전달 개선 (userId 포함)
  • 기타

    • 화면 상태(앱바 제목 등) 및 프로필 상태 노출 확장

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

@nahy-512 nahy-512 self-assigned this Jan 22, 2026
@nahy-512 nahy-512 requested a review from a team as a code owner January 22, 2026 00:06
@nahy-512 nahy-512 added the Feat ✨ 신규 기능을 추가하거나 기존 기능의 동작, 정책을 변경 label Jan 22, 2026
@nahy-512 nahy-512 linked an issue Jan 22, 2026 that may be closed by this pull request
3 tasks
@coderabbitai

coderabbitai Bot commented Jan 22, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

프로필 라우트와 Route 모델을 분리·변경하고, 소셜 인증 응답 타입 및 저장 로직을 수정했습니다. 프로필 화면에 userId 기반 조회와 OTT 바텀시트(사이드이펙트) 추가, 컬렉션 리스트에 userId 전달·북마크 디바운스 로직이 도입되었습니다.

Changes

Cohort / File(s) 변경 요약
네비게이션 / Route 변경
app/src/main/java/com/flint/core/navigation/MainTabRoute.kt, app/src/main/java/com/flint/core/navigation/Route.kt
MainTabRoute.Profile을 singleton object로 변경. Route에 Profile(val userId: String? = null) 추가 및 CollectionListuserId 필드 추가.
소셜 인증 DTO/모델/저장
app/src/main/java/com/flint/data/dto/auth/response/SocialVerifyResponseDto.kt, app/src/main/java/com/flint/domain/mapper/auth/SocialVerifyMapper.kt, app/src/main/java/com/flint/domain/model/auth/SocialVerifyModel.kt, app/src/main/java/com/flint/domain/repository/AuthRepository.kt, app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt
SocialVerify 응답 userId 타입을 Long?→String?, nickNamenickname으로 변경. socialVerify 흐름에서 ACCESS_TOKEN/USER_ID/USER_NAME을 PreferencesManager에 저장하도록 추가. SplashViewModel에 저장용 메서드 추가.
User API & UserRepository
app/src/main/java/com/flint/data/api/UserApi.kt, app/src/main/java/com/flint/domain/repository/UserRepository.kt
사용자별 북마크 콘텐츠 및 "내" 컬렉션 엔드포인트 추가. UserRepository에 PreferencesManager 주입, 저장된 USER_ID 조회 헬퍼 추가 및 userId 기반 호출로 분기; getUserBookmarkedContents 신규 추가.
Profile 화면·VM·사이드이펙트·UIState
app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt, app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt, app/src/main/java/com/flint/presentation/profile/navigation/ProfileNavigation.kt, app/src/main/java/com/flint/presentation/profile/sideeffect/ProfileSideEffect.kt, app/src/main/java/com/flint/presentation/profile/uistate/ProfileUiState.kt
ProfileViewModel이 SavedStateHandle로 userId 노출, SharedFlow 기반 sideEffect 추가(ShowOttListBottomSheet). ProfileScreen에 OTT 바텀시트 표시 로직 및 navigateToCollectionList에 userId 전달 반영. ProfileUiState에 userId 필드 추가. navigation에 myProfileNavGraph 분리/overload 추가.
컬렉션 리스트/뷰모델/네비게이션 연결
app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt, app/src/main/java/com/flint/presentation/collectionlist/navigation/CollectionListNavigation.kt, app/src/main/java/com/flint/presentation/collectionlist/CollectionListScreen.kt, app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt
CollectionListViewModel이 routeReceiveData(userId 포함) 사용으로 변경. navigateToCollectionList에 userId 파라미터 추가. 북마크 토글에 500ms 디바운스·낙관적 업데이트 및 실패 시 롤백 로직 추가. CollectionDetailScreen에 onAuthorNicknameClick 콜백 추가.
메인 네비·탭 변경
app/src/main/java/com/flint/presentation/main/MainNavHost.kt, app/src/main/java/com/flint/presentation/main/MainNavigator.kt, app/src/main/java/com/flint/presentation/main/MainTab.kt
myProfileNavGraph 추가 및 호출, startDestination을 Route.Splash로 변경. MainTab에서 Profile 참조를 객체로 사용하도록 변경. navigateToCollectionList에 userId 인자 추가.
기타
app/src/main/java/com/flint/domain/repository/ContentRepository.kt, app/src/main/java/com/flint/data/di/interceptor/TokenInterceptor.kt, app/src/main/java/com/flint/presentation/home/HomeScreen.kt, app/src/main/java/com/flint/presentation/home/HomeViewModel.kt, app/src/main/java/com/flint/presentation/login/LoginViewModel.kt
ContentRepository 포맷 정리. TokenInterceptor의 하드코딩된 샘플 토큰 제거(실제 accessToken만 추가). HomeScreen의 userName을 필수 파라미터로 변경. 일부 Preferences 관련 import/사용 정리 및 불필요 import 제거.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SplashVM as SplashViewModel
    participant Prefs as PreferencesManager
    participant Nav as MainNavigator
    participant ProfileVM as ProfileViewModel
    participant UserRepo as UserRepository
    participant ContentRepo as ContentRepository
    participant ProfileUI as ProfileScreen

    User->>SplashVM: 앱 시작
    SplashVM->>Prefs: ACCESS_TOKEN/USER_ID/USER_NAME 저장
    SplashVM->>Nav: 초기 네비게이션(Route.Splash)

    User->>Nav: 프로필 탭/프로필 진입 (userId 선택적)
    Nav->>ProfileVM: Profile 화면 생성 (SavedStateHandle.userId)
    ProfileVM->>UserRepo: getProfile(userId)
    UserRepo-->>ProfileVM: Profile 데이터 반환
    ProfileVM->>UserRepo: 컬렉션/북마크/키워드 요청 (userId)
    UserRepo-->>ProfileVM: 섹션 데이터 반환
    ProfileVM->>ProfileUI: ProfileUiState 발행

    User->>ProfileUI: 콘텐츠 클릭
    ProfileUI->>ProfileVM: getOttListPerContent(contentId)
    ProfileVM->>ContentRepo: OTT 목록 요청
    ContentRepo-->>ProfileVM: OttListModel 반환
    ProfileVM-->>ProfileUI: ProfileSideEffect.ShowOttListBottomSheet(ottList)
    ProfileUI->>ProfileUI: OTT 바텀시트 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • giovannijunseokim
  • kimjw2003

Poem

🐰 깡충 깡충 코드 밭을 뛰며,
라우트는 깔끔히 하나로 모았네.
토큰은 쏙쏙 저장, 프로필은 누가 봐도 명확히,
클릭하면 OTT가 펑 — 바텀시트로 춤추네.
당근 들고 축하하자, 벌레 없는 머지들!

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 변경사항 중 일부가 #157의 요구사항 범위를 벗어납니다: userId 타입 변경(Long→String), 컬렉션 리스트 화면의 userId 매개변수 추가, SocialVerifyResponseDto 필드 변경 등이 #157의 범위를 초과합니다. userId 타입 변경 및 컬렉션 리스트 화면 변경이 프로필 기능과의 연관성을 명확히 설명하거나 별도 이슈로 분리하는 것을 검토해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 32.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 프로필 추가 로직 구현이라는 핵심 변경사항을 명확히 반영하고 있어 전체 변경사항의 주요 목적을 잘 설명합니다.
Description check ✅ Passed PR 설명이 관련 이슈, 작업 내용, 스크린샷, 리뷰어 참고사항을 모두 포함하여 템플릿 요구사항을 충족하고 있습니다.
Linked Issues check ✅ Passed PR의 주요 변경사항들이 #157의 목표를 충족합니다: userId 기반 내/타인 프로필 구분 구현, OTT 바텀시트 추가, 프로필 API 연동이 모두 포함되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (2)
app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt (2)

43-59: getProfile() 실패 시 에러 처리 누락

onSuccess만 처리하고 onFailure가 없어, API 호출 실패 시 사용자에게 아무런 피드백이 없고 UI가 UiState.Empty 상태로 유지됩니다.

🔧 에러 처리 추가 제안
             }.onSuccess { combinedState ->
                 _uiState.update { UiState.Success(combinedState) }
                 getSectionInfo()
+            }.onFailure { throwable ->
+                Timber.e(throwable)
+                _uiState.update { UiState.Error(throwable) }
             }

61-85: getSectionInfo() 실패 시 에러 처리 누락

onSuccess만 처리하고 있어, 섹션 정보 로딩 실패 시 사용자가 인지할 수 없습니다. getOttListPerContent처럼 최소한 로깅을 추가하거나 에러 상태를 UI에 반영해야 합니다.

🔧 에러 처리 추가 제안
             }.onSuccess { updatedState ->
                 _uiState.update { UiState.Success(updatedState) }
+            }.onFailure { throwable ->
+                Timber.e(throwable)
+                // 필요시 에러 상태 UI 반영 또는 사이드 이펙트 emit
             }
🤖 Fix all issues with AI agents
In `@app/src/main/java/com/flint/domain/repository/AuthRepository.kt`:
- Line 15: AuthRepository.kt에 선언된 사용되지 않는 import인 Timber를 제거하세요; 불필요한 import는 빌드
경고를 유발하므로 파일 상단의 "import timber.log.Timber" 구문을 삭제하고, 프로젝트 전반에서 Timber를 실제로
사용하려면 해당 로깅 호출(예: AuthRepository 클래스 내부의 로그 위치)을 추가하는 대신 단순히 import만 제거하면 됩니다.
- Around line 25-32: 현재 socialVerify 함수에서 api.socialVerify(...).data.toModel()
호출과 preferencesManager에 값 저장이 suspendRunCatching 블록 바깥에 있어 네트워크/변환 예외를 잡지 못하고
null 값이 "null"로 저장됩니다; 수정 방법은 socialVerify 내부에서 suspendRunCatching 블록을 사용해
api.socialVerify(model.toDto()) 호출, data.toModel() 변환, 그리고
preferencesManager.saveString(ACCESS_TOKEN/USER_NAME/USER_ID, ...) 저장을 모두 그 블록
안으로 옮기고 null-safe 저장(예: result.accessToken?.let { saveString(ACCESS_TOKEN, it) }
또는 기본값 사용)으로 "null" 문자열이 저장되지 않게 하며 최종적으로 suspendRunCatching 블록의 결과를 반환하도록
변경하세요.

In `@app/src/main/java/com/flint/domain/repository/ContentRepository.kt`:
- Around line 22-24: The myUserId function uses runBlocking which blocks threads
and duplicates the same pattern from UserRepository; change this to a
suspend-friendly approach by either making myUserId a suspend function (suspend
fun myUserId(): String) that calls preferencesManager.getString(USER_ID).first()
or add a suspend helper on PreferencesManager (e.g., suspend fun
getStringOnce(key: String): String) and call that from ContentRepository; remove
runBlocking usage and any duplicate logic so both repositories reuse the same
suspend helper (refer to myUserId, PreferencesManager, and USER_ID to locate the
code).
- Around line 11-16: Remove the unused and conflicting imports in
ContentRepository.kt: delete the unused kotlinx.coroutines.launch import and
remove kotlin.text.first (which conflicts with kotlinx.coroutines.flow.first);
keep kotlinx.coroutines.flow.first for flow operations and retain Timber and
Inject imports as needed so the file compiles without ambiguous references.
- Around line 31-37: The function getBookmarkedContentListByUserId currently
ignores the resolved user and passes a hardcoded "801159854933808613" to
apiService.getBookmarkedContentListByUserId; replace that hardcoded id with the
resolved value (userId ?: myUserId()) when calling the API and ensure myUserId()
is invoked correctly in this suspend context, and remove the unused commented
runBlocking block (the commented lines between Line 33-35). Target symbols:
getBookmarkedContentListByUserId, myUserId(), and
apiService.getBookmarkedContentListByUserId.

In `@app/src/main/java/com/flint/domain/repository/UserRepository.kt`:
- Around line 21-23: The helper myUserId currently blocks threads via
runBlocking; change it to a suspend function (suspend fun myUserId(): String)
and call preferencesManager.getString(USER_ID).first() directly from coroutine
callers; also remove any other runBlocking-based helpers in ContentRepository
and either extract a shared suspend utility (e.g.,
preferences.getUserIdSuspend()) or add a suspend accessor to PreferencesManager
so callers use the suspend API instead of blocking.

In `@app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt`:
- Around line 32-33: The crash happens because ProfileViewModel uses
savedStateHandle.toRoute<Route.Profile>() which only works when the current
route is Route.Profile; when entering via MainTabRoute.Profile
(myProfileNavGraph) this throws IllegalArgumentException—update the ViewModel to
handle both route types by checking the actual route instance (e.g., inspect
savedStateHandle or use toRoute for MainTabRoute.Profile when applicable) and
extract userId from either Route.Profile or MainTabRoute.Profile, or add a
guard/fallback before calling savedStateHandle.toRoute<Route.Profile>() so a
missing userId defaults to the current user's id; adjust ProfileViewModel (and
any userId usage) to branch on the route type and safely parse userId without
throwing.

In `@app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt`:
- Line 32: In SplashViewModel remove or stop logging the raw USER_ID from
prefData.first() (the Timber.d call) to avoid PII exposure; either delete the
Timber.d("prefData: ${prefData.first()}") line or replace it with a
non-identifying message (e.g., log a boolean like "hasUserId" or a masked/hashed
version of prefData.first()) and ensure any masking uses a one-way hash function
so the original ID cannot be reconstructed.
- Around line 20-32: prefData is accessed inside sampleSaveData() which is
called from init before prefData is initialized, risking an NPE; fix by ensuring
prefData is initialized before init runs — either move the val prefData =
preferencesManager.getString(USER_ID) above the init block or declare it lazy
(e.g., val prefData by lazy { preferencesManager.getString(USER_ID) }) so access
in sampleSaveData() is safe; alternatively, avoid reading prefData in
sampleSaveData() and obtain the flow/value inside that coroutine (use
preferencesManager.getString(USER_ID).first() within sampleSaveData()) to
guarantee correct initialization.
🧹 Nitpick comments (5)
app/src/main/java/com/flint/data/local/PreferencesManager.kt (1)

43-46: 빈 문자열 대신 null/공유 키 사용 고려
미설정 시 "" 반환은 호출부에서 유효성 판단이 어렵습니다. 키 상수화와 String? 반환으로 의미를 명확히 하는 편이 안전합니다.

♻️ 제안 변경
+    private companion object {
+        const val USER_ID_KEY = "USER_ID"
+    }
+
-    suspend fun getUserId(): String {
-        return dataStore.data.map { preferences ->
-            preferences[stringPreferencesKey("USER_ID")] ?: ""
-        }.first()
-    }
+    suspend fun getUserId(): String? =
+        dataStore.data.map { preferences ->
+            preferences[stringPreferencesKey(USER_ID_KEY)]
+        }.first()
app/src/main/java/com/flint/presentation/profile/sideeffect/ProfileSideEffect.kt (1)

1-7: SideEffect는 sealed interface로 제한 권장
추후 분기 처리에서 컴파일 타임 안전성을 확보하려면 sealed interface가 더 적합합니다.

♻️ 제안 변경
-interface ProfileSideEffect {
+sealed interface ProfileSideEffect {
     data class ShowOttListBottomSheet(val ottListModel: OttListModel) : ProfileSideEffect
 }
app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt (1)

588-595: 작성자 닉네임 클릭 영역 구현 확인

noRippleClickable로 닉네임 텍스트에 클릭 이벤트가 연결되었습니다. 클릭 가능함을 사용자에게 시각적으로 전달하는 UI 힌트(예: 밑줄, 색상 변경)가 없는 점은 UX 측면에서 고려해볼 수 있습니다.

app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt (1)

101-110: openUri 호출 시 예외 처리 고려

uriHandler.openUri(url)은 잘못된 URL이나 핸들러가 없는 경우 ActivityNotFoundException을 발생시킬 수 있습니다. 방어적 코드 추가를 고려해 보세요.

♻️ 방어적 코드 제안
                 onMoveClick = { url ->
-                    uriHandler.openUri(url)
+                    try {
+                        uriHandler.openUri(url)
+                    } catch (e: Exception) {
+                        Timber.e(e, "Failed to open URL: $url")
+                    }
                 },
app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt (1)

27-31: 프로덕션에서 샘플 키를 매번 저장하는 부작용
sampleKey/sampleValue 저장이 매번 실행됩니다. 의도된 동작이 아니라면 디버그 전용으로 가드하거나 제거하는 편이 안전합니다.

🧹 제안 수정안
-                preferencesManager.saveString("sampleKey", "sampleValue")
+                // TODO: 디버그 목적이 아니라면 제거하거나 실제 USER_ID/USER_NAME 저장 로직으로 대체

Comment thread app/src/main/java/com/flint/domain/repository/AuthRepository.kt Outdated
Comment thread app/src/main/java/com/flint/domain/repository/AuthRepository.kt Outdated
Comment thread app/src/main/java/com/flint/domain/repository/ContentRepository.kt Outdated
Comment thread app/src/main/java/com/flint/domain/repository/ContentRepository.kt Outdated
Comment thread app/src/main/java/com/flint/domain/repository/ContentRepository.kt Outdated
Comment thread app/src/main/java/com/flint/domain/repository/UserRepository.kt Outdated
Comment on lines +32 to +33

val userId = savedStateHandle.toRoute<Route.Profile>().userId

@coderabbitai coderabbitai Bot Jan 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

MainTabRoute.Profile로 진입 시 런타임 크래시 발생

savedStateHandle.toRoute<Route.Profile>()은 현재 라우트가 Route.Profile일 때만 동작합니다. 그러나 myProfileNavGraph에서는 MainTabRoute.Profile을 사용하고 있어, 하단 탭을 통해 내 프로필에 진입하면 IllegalArgumentException이 발생합니다.

내 프로필/타인 프로필을 구분하여 처리해야 합니다:

🔧 수정 제안
-    val userId = savedStateHandle.toRoute<Route.Profile>().userId
+    val userId: String? = try {
+        savedStateHandle.toRoute<Route.Profile>().userId
+    } catch (e: Exception) {
+        // MainTabRoute.Profile로 진입한 경우 (내 프로필)
+        null
+    }

또는 MainTabRoute.ProfileRoute.Profile을 각각 처리하는 별도의 ViewModel을 고려하거나, 두 라우트 타입 모두에서 userId를 추출할 수 있도록 로직을 수정해야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val userId = savedStateHandle.toRoute<Route.Profile>().userId
val userId: String? = try {
savedStateHandle.toRoute<Route.Profile>().userId
} catch (e: Exception) {
// MainTabRoute.Profile로 진입한 경우 (내 프로필)
null
}
🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt` around
lines 32 - 33, The crash happens because ProfileViewModel uses
savedStateHandle.toRoute<Route.Profile>() which only works when the current
route is Route.Profile; when entering via MainTabRoute.Profile
(myProfileNavGraph) this throws IllegalArgumentException—update the ViewModel to
handle both route types by checking the actual route instance (e.g., inspect
savedStateHandle or use toRoute for MainTabRoute.Profile when applicable) and
extract userId from either Route.Profile or MainTabRoute.Profile, or add a
guard/fallback before calling savedStateHandle.toRoute<Route.Profile>() so a
missing userId defaults to the current user's id; adjust ProfileViewModel (and
any userId usage) to branch on the route type and safely parse userId without
throwing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

확인해봤는데 userId null로 들어가서 괜찮던데?

Comment thread app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt Outdated
Comment thread app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (2)
app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt (2)

44-63: getProfile() 실패 시 에러 처리가 누락되었습니다.

suspendRunCatchingonFailure 블록이 없어 API 호출 실패 시 UI 상태가 UiState.Empty로 유지됩니다. 사용자에게 에러 상태를 표시하거나 적절한 피드백을 제공해야 합니다.

🔧 에러 처리 추가 제안
             }.onSuccess { combinedState ->
                 _uiState.update { UiState.Success(combinedState) }
                 getSectionInfo()
+            }.onFailure { throwable ->
+                Timber.e(throwable)
+                _uiState.update { UiState.Error(throwable) }
             }

66-89: getSectionInfo() 실패 시 에러 처리가 누락되었습니다.

섹션 정보 조회 실패 시 onFailure 처리가 없습니다. 프로필은 성공적으로 로드되었지만 섹션 정보만 실패한 경우, 사용자에게 부분 실패 상태를 알리거나 재시도 옵션을 제공하는 것이 좋습니다.

🔧 에러 로깅 추가 제안
             }.onSuccess { updatedState ->
                 _uiState.update { UiState.Success(updatedState) }
+            }.onFailure { throwable ->
+                Timber.e(throwable, "Failed to load section info")
             }
♻️ Duplicate comments (1)
app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt (1)

33-34: MainTabRoute.Profile로 진입 시 런타임 크래시 발생

이전 리뷰에서 지적된 것처럼, savedStateHandle.toRoute<Route.Profile>()은 하단 탭 네비게이션(MainTabRoute.Profile)을 통해 진입할 때 IllegalArgumentException을 발생시킵니다.

@nahy-512 nahy-512 changed the title [Feat] 프로필 추가 로직 구현 [Feat/#157] 프로필 추가 로직 구현 Jan 22, 2026
@nahy-512 nahy-512 added the 🔖 API feat - API 연동 label Jan 22, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt`:
- Around line 19-21: The init block in SplashViewModel currently invokes
saveTempUserData() causing hardcoded test credentials to be stored on every
ViewModel creation; remove that direct call from the init block or guard it
behind a debug-only condition (e.g., check a BuildConfig.DEBUG or an injected
debug flag) so saveTempUserData() runs only in development/testing, not in
production; update SplashViewModel's init to omit the unconditional call and, if
using a guard, reference saveTempUserData() and the chosen debug flag/Class so
behavior is restricted to non-production builds.
- Line 17: Remove the unused property prefData that assigns
preferencesManager.getString("sampleKey") from the SplashViewModel class; locate
the declaration of prefData in SplashViewModel and delete it (and any now-unused
imports or backing fields if present) to avoid holding an unused reference to
preferencesManager.
- Around line 27-31: The saveTempUserData function currently writes hardcoded
sensitive values (ACCESS_TOKEN, USER_ID, USER_NAME) via preferencesManager;
remove these literals and instead read injected/test-safe values (e.g., from
BuildConfig, environment/local.properties accessed via BuildConfig fields, or a
test-only MockPreferences implementation) or generate dummy non-sensitive values
for local dev; update saveTempUserData to accept parameters or fetch from a
secure source (e.g., PreferencesRepository.getTempCredentials or
constructor-injected tokenProvider) and ensure any real tokens committed are
revoked outside this change.
🧹 Nitpick comments (2)
app/src/main/java/com/flint/presentation/home/HomeScreen.kt (1)

124-125: 빈 문자열 userName 처리 고려

userName의 기본값을 제거한 것은 좋은 변경이지만, userName이 빈 문자열일 경우 195번 줄의 설명 텍스트가 "님이 최근 살펴본 컬렉션이에요"로 표시되어 문법적으로 어색해질 수 있습니다.

필요시 빈 문자열에 대한 fallback 처리를 고려해 주세요.

💡 선택적 개선안
 CollectionSection(
     title = "눈여겨보고 있는 컬렉션",
-    description = "${userName}님이 최근 살펴본 컬렉션이에요",
+    description = if (userName.isNotBlank()) "${userName}님이 최근 살펴본 컬렉션이에요" else "최근 살펴본 컬렉션이에요",
     isAllVisible = true,

Also applies to: 195-195

app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt (1)

23-25: 미사용 테스트 코드 정리 권장

sampleSaveData() 함수는 "sampleKey"/"sampleValue"를 저장하는 테스트용 코드로 보입니다. 현재 호출되는 곳이 없다면 제거를 권장합니다.

♻️ 제안: 미사용 테스트 함수 제거
-    fun sampleSaveData() = viewModelScope.launch {
-        preferencesManager.saveString("sampleKey", "sampleValue")
-    }

Comment thread app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt Outdated
Comment thread app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt Outdated
Comment on lines +27 to +31
fun saveTempUserData() = viewModelScope.launch {
preferencesManager.saveString(ACCESS_TOKEN, "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjgwMDAyMTcxMTM1NzExODE5MSwicm9sZSI6IkZMSU5HIiwidHlwZSI6IkFDQ0VTUyIsImlhdCI6MTc2ODgzODU0MCwiZXhwIjoxNzcwMDQ4MTQwfQ.A2XYyu24IoGAXNSQHJ1S-iudWmg8II2_ivI4EdyyWw9KS9oJlxHKOAhcKrsLpLkc9kllZyxwaTJO1t4vI7oZlg")
preferencesManager.saveString(USER_ID, "800021711357118191")
preferencesManager.saveString(USER_NAME, "hojoo")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🚨 보안 취약점: 하드코딩된 JWT 토큰 및 사용자 자격 증명

실제 JWT 액세스 토큰, 사용자 ID, 사용자 이름이 소스 코드에 직접 포함되어 있습니다. 이 코드가 버전 관리에 커밋되면 토큰이 노출되어 무단 접근 위험이 발생합니다.

임시 테스트 목적이라면 다음 방법을 고려해 주세요:

  • local.properties 또는 환경 변수를 통해 민감한 값을 주입
  • 테스트용 Mock/Fake 구현 사용
  • 프로덕션 빌드에서는 실제 인증 플로우 사용

이미 커밋된 토큰은 즉시 무효화(revoke)해야 합니다.

🔧 제안: 하드코딩된 자격 증명 제거
-    fun saveTempUserData() = viewModelScope.launch {
-        preferencesManager.saveString(ACCESS_TOKEN, "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjgwMDAyMTcxMTM1NzExODE5MSwicm9sZSI6IkZMSU5HIiwidHlwZSI6IkFDQ0VTUyIsImlhdCI6MTc2ODgzODU0MCwiZXhwIjoxNzcwMDQ4MTQwfQ.A2XYyu24IoGAXNSQHJ1S-iudWmg8II2_ivI4EdyyWw9KS9oJlxHKOAhcKrsLpLkc9kllZyxwaTJO1t4vI7oZlg")
-        preferencesManager.saveString(USER_ID, "800021711357118191")
-        preferencesManager.saveString(USER_NAME, "hojoo")
-    }
+    // TODO: 실제 로그인 플로우에서 서버로부터 받은 토큰을 저장하도록 변경
+    fun saveUserData(accessToken: String, userId: String, userName: String) = viewModelScope.launch {
+        preferencesManager.saveString(ACCESS_TOKEN, accessToken)
+        preferencesManager.saveString(USER_ID, userId)
+        preferencesManager.saveString(USER_NAME, userName)
+    }
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 28-28: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt` around
lines 27 - 31, The saveTempUserData function currently writes hardcoded
sensitive values (ACCESS_TOKEN, USER_ID, USER_NAME) via preferencesManager;
remove these literals and instead read injected/test-safe values (e.g., from
BuildConfig, environment/local.properties accessed via BuildConfig fields, or a
test-only MockPreferences implementation) or generate dummy non-sensitive values
for local dev; update saveTempUserData to accept parameters or fetch from a
secure source (e.g., PreferencesRepository.getTempCredentials or
constructor-injected tokenProvider) and ensure any real tokens committed are
revoked outside this change.

@imflint imflint deleted a comment from coderabbitai Bot Jan 22, 2026

@giovannijunseokim giovannijunseokim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 👍

) : ViewModel() {

val routeType = savedStateHandle.toRoute<Route.CollectionList>().routeType
val routeReceiveData = savedStateHandle.toRoute<Route.CollectionList>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

p3: 화면에 표시할 데이터가 담겨있는 것 같아요. uiState로 한 번에 관리할 수 있나요? 👀

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

uiState로 관리해줘야 하는 데이터일까? 싶어서 따로 설정해줬던 건데, 수정하는 편이 좋을 것 같네요ㅎㅎ 감사합니다

@kimjw2003 kimjw2003 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 Code Review Summary

전반적으로 프로필 기능 구현이 잘 되었습니다. 다만 몇 가지 Critical한 이슈들이 있어 수정이 필요합니다.

🔴 Critical Issues

1. Exception Handling 범위 문제 (AuthRepository.kt)

API 호출과 preference 저장이 suspendRunCatching 블록 밖에서 실행되어 예외 처리가 누락되었습니다. 네트워크 실패 시 앱 크래시 가능성이 있습니다.

2. runBlocking 사용 (UserRepository.kt)

코루틴 컨텍스트에서 runBlocking을 사용하면 스레드풀 고갈 및 데드락 위험이 있습니다. suspend fun으로 변경이 필요합니다.

⚠️ Major Issues

  • 사용하지 않는 import (AuthRepository.kt - Timber)
  • 하드코딩된 테스트 토큰 제거 필요 (TokenInterceptor.kt)

구체적인 수정 방안은 inline comments를 확인해주세요.

@kimjw2003

Copy link
Copy Markdown
Contributor

🔴 Critical Issue #1: Exception Handling (AuthRepository.kt)

// ❌ 현재 코드
suspend fun socialVerify(model: SocialVerifyRequestModel): Result<SocialVerifyResponseModel> {
    val result = api.socialVerify(model.toDto()).data.toModel()
    preferencesManager.saveString(ACCESS_TOKEN, result.accessToken.toString())
    // ...
    return suspendRunCatching { result }
}

문제점:

  • API 호출과 preference 저장이 suspendRunCatching 블록 밖에서 실행
  • 네트워크 실패 시 앱 크래시 위험
  • null 값을 "null" 문자열로 저장 가능

수정 제안:

// ✅ 수정 코드
suspend fun socialVerify(model: SocialVerifyRequestModel): Result<SocialVerifyResponseModel> =
    suspendRunCatching {
        val result = api.socialVerify(model.toDto()).data.toModel()
        result.accessToken?.let { preferencesManager.saveString(ACCESS_TOKEN, it) }
        result.userId?.let { preferencesManager.saveString(USER_ID, it) }
        result.userName?.let { preferencesManager.saveString(USER_NAME, it) }
        result
    }

@kimjw2003

Copy link
Copy Markdown
Contributor

🔴 Critical Issue #2: runBlocking in Coroutine Context (UserRepository.kt)

// ❌ 현재 코드
private fun myUserId(): String = runBlocking {
    preferencesManager.getString(USER_ID).first()
}

문제점:

  • 코루틴 컨텍스트에서 runBlocking 사용 시 스레드풀 고갈 위험
  • 데드락 가능성
  • 성능 저하

수정 제안:

// ✅ 수정 코드
private suspend fun myUserId(): String {
    return preferencesManager.getString(USER_ID).first()
}

이 함수를 호출하는 모든 곳도 suspend context에서 호출하도록 수정해주세요.

@kimjw2003

Copy link
Copy Markdown
Contributor

⚠️ Major Issues

1. 사용하지 않는 import (AuthRepository.kt)

import timber.log.Timber  // ❌ 사용되지 않음 - 제거 필요

2. 하드코딩된 테스트 토큰 (TokenInterceptor.kt)

프로덕션 배포 전 반드시 제거해야 합니다.

// ❌ 제거 필요
requestBuilder.header("Authorization", "Bearer eyJhbGciOi...")

테스트용 토큰이 필요한 경우 DI를 통한 주입 방식으로 변경하는 것을 권장합니다.

3. userId 타입 변경 (Long → String)

백엔드 API 스펙과 일치하는지 확인이 필요합니다.

@kimjw2003

Copy link
Copy Markdown
Contributor

✅ 잘 구현된 부분

  1. OTT 바텀시트 구현: SharedFlow 기반 SideEffect 패턴을 사용하여 일회성 이벤트를 처리한 설계가 훌륭합니다.

  2. 프로필 라우팅: userId 기반으로 본인/타인 프로필을 구분하는 로직이 깔끔하게 구현되었습니다.

  3. Debouncing 추가: 북마크 토글에 500ms debouncing을 추가하여 연속 클릭 문제를 방지한 점이 좋습니다.

  4. Rollback 로직: 북마크 토글 실패 시 UI를 이전 상태로 되돌리는 로직이 사용자 경험을 개선합니다.


머지 권장사항: Critical Issues (1, 2번)를 먼저 수정하고, 테스트 토큰을 제거한 후 머지하는 것을 권장합니다. 🚀

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/src/main/java/com/flint/domain/repository/UserRepository.kt`:
- Around line 58-60: The current getUserBookmarkedContents(userId: String?) in
UserRepository uses apiService.getBookmarkedContentListByUserId(userId ?:
myUserId()), which can miss private/personal-only bookmarks because there's no
dedicated my-endpoint; update the repository to branch on null userId and call a
new apiService.getMyBookmarkedContents() when requesting the current user's
bookmarks (and fall back to getBookmarkedContentListByUserId(userId) when userId
is provided), and ensure the API layer exposes and implements
getMyBookmarkedContents() so access-control/private bookmarks are returned
correctly.
♻️ Duplicate comments (1)
app/src/main/java/com/flint/presentation/splash/SplashViewModel.kt (1)

21-24: 하드코딩된 토큰/사용자 정보는 즉시 제거하세요.
실제 자격 증명이 소스에 포함되어 있어 보안 위험이 큽니다. 테스트 목적이라면 주입 방식으로 변경하고, 커밋된 토큰은 폐기(회수)해 주세요.

🔧 제안 수정안
-    fun saveTempUserData() = viewModelScope.launch {
-        preferencesManager.saveString(ACCESS_TOKEN, "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjgwMDAyMTcxMTM1NzExODE5MSwicm9sZSI6IkZMSU5HIiwidHlwZSI6IkFDQ0VTUyIsImlhdCI6MTc2ODgzODU0MCwiZXhwIjoxNzcwMDQ4MTQwfQ.A2XYyu24IoGAXNSQHJ1S-iudWmg8II2_ivI4EdyyWw9KS9oJlxHKOAhcKrsLpLkc9kllZyxwaTJO1t4vI7oZlg")
-        preferencesManager.saveString(USER_ID, "800021711357118191")
-        preferencesManager.saveString(USER_NAME, "hojoo")
-    }
+    fun saveUserData(
+        accessToken: String,
+        userId: String,
+        userName: String
+    ) = viewModelScope.launch {
+        preferencesManager.saveString(ACCESS_TOKEN, accessToken)
+        preferencesManager.saveString(USER_ID, userId)
+        preferencesManager.saveString(USER_NAME, userName)
+    }
🧹 Nitpick comments (1)
app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt (1)

134-181: 두 헬퍼 함수의 중복 로직을 통합할 수 있습니다.

updateCollectionBookmarkStateupdateCollectionIsBookmarkedOnly가 유사한 구조를 가지고 있습니다. 선택적 bookmarkCount 파라미터를 사용하여 하나의 함수로 통합할 수 있습니다.

♻️ 선택적 리팩토링 제안
-    private fun updateCollectionBookmarkState(
-        collectionId: String,
-        isBookmarked: Boolean,
-        bookmarkCount: Int,
-    ) {
+    private fun updateCollectionBookmarkState(
+        collectionId: String,
+        isBookmarked: Boolean,
+        bookmarkCount: Int? = null,
+    ) {
         _uiState.update { currentState: CollectionListUiState ->
             val collectionList = currentState.collectionList
             if (collectionList !is UiState.Success) return@update currentState
 
             val updatedCollections = collectionList.data.collections.map { collection ->
                 if (collection.id == collectionId) {
-                    collection.copy(
-                        isBookmarked = isBookmarked,
-                        bookmarkCount = bookmarkCount
-                    )
+                    if (bookmarkCount != null) {
+                        collection.copy(
+                            isBookmarked = isBookmarked,
+                            bookmarkCount = bookmarkCount
+                        )
+                    } else {
+                        collection.copy(isBookmarked = isBookmarked)
+                    }
                 } else {
                     collection
                 }
             }.toImmutableList()
 
             currentState.copy(
                 collectionList = UiState.Success(
                     CollectionListModel(collections = updatedCollections)
                 )
             )
         }
     }
-
-    private fun updateCollectionIsBookmarkedOnly(collectionId: String, isBookmarked: Boolean) {
-        _uiState.update { currentState: CollectionListUiState ->
-            val collectionList = currentState.collectionList
-            if (collectionList !is UiState.Success) return@update currentState
-
-            val updatedCollections = collectionList.data.collections.map { collection ->
-                if (collection.id == collectionId) {
-                    collection.copy(isBookmarked = isBookmarked)
-                } else {
-                    collection
-                }
-            }.toImmutableList()
-
-            currentState.copy(
-                collectionList = UiState.Success(
-                    CollectionListModel(collections = updatedCollections)
-                )
-            )
-        }
-    }

Comment on lines +58 to +60
// 사용자별 북마크한 콘텐츠 목록 조회
suspend fun getUserBookmarkedContents(userId: String?) : Result<BookmarkedContentListModel> =
suspendRunCatching { apiService.getBookmarkedContentListByUserId(userId ?: myUserId()).data.toModel() }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for bookmarked content endpoints in UserApi or similar
echo "=== Searching for 'getMy' bookmarked endpoints ==="
rg -n 'getMy.*[Bb]ookmarked.*[Cc]ontent' app/src/main/java --type kotlin

echo ""
echo "=== Searching for 'getBookmarkedContentListByUserId' endpoint ==="
rg -n 'getBookmarkedContentListByUserId' app/src/main/java --type kotlin

echo ""
echo "=== Searching for all bookmarked content related methods ==="
rg -n '[Bb]ookmarked[Cc]ontent' app/src/main/java/com/flint/domain/repository/UserRepository.kt -A 1 -B 1

Repository: imflint/Flint-Android

Length of output: 964


🏁 Script executed:

#!/bin/bash
echo "=== Checking UserApi.kt for bookmarked content endpoints ==="
cat -n app/src/main/java/com/flint/data/api/UserApi.kt | grep -A 3 -B 1 -i bookmark

echo ""
echo "=== Checking for collection methods pattern (mentioned in comment) ==="
rg -n 'getMyCollections|getCollectionsByUserId' app/src/main/java/com/flint/domain/repository/UserRepository.kt -A 1

Repository: imflint/Flint-Android

Length of output: 1600


내 프로필용 북마크 콘텐츠 분기 누락 가능성

컬렉션과 달리 북마크 콘텐츠는 API에 getMyBookmarkedContents() 엔드포인트가 없습니다. UserApi에는 getMyBookmarkedCollections()는 있지만, 북마크 콘텐츠는 getBookmarkedContentListByUserId(userId)만 존재합니다.

현재 구현은 userId ?: myUserId()로 우회하지만, 북마크 콘텐츠가 사용자 전용(비공개 포함)이라면 전용 엔드포인트의 부재로 인해 데이터 누락이나 접근 제어 문제가 발생할 수 있습니다. API에 getMyBookmarkedContents() 엔드포인트를 추가하고 분기 로직을 구현해야 합니다.

🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/domain/repository/UserRepository.kt` around lines
58 - 60, The current getUserBookmarkedContents(userId: String?) in
UserRepository uses apiService.getBookmarkedContentListByUserId(userId ?:
myUserId()), which can miss private/personal-only bookmarks because there's no
dedicated my-endpoint; update the repository to branch on null userId and call a
new apiService.getMyBookmarkedContents() when requesting the current user's
bookmarks (and fall back to getBookmarkedContentListByUserId(userId) when userId
is provided), and ensure the API layer exposes and implements
getMyBookmarkedContents() so access-control/private bookmarks are returned
correctly.

@nahy-512 nahy-512 merged commit 4334cde into develop Jan 22, 2026
2 checks passed
@nahy-512 nahy-512 deleted the feat/#157-profile-api-detail branch January 22, 2026 11:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔖 API feat - API 연동 Feat ✨ 신규 기능을 추가하거나 기존 기능의 동작, 정책을 변경

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] Profile API 디테일 추가

3 participants