[Feat/#157] 프로필 추가 로직 구현#163
Conversation
📝 WalkthroughWalkthrough프로필 라우트와 Route 모델을 분리·변경하고, 소셜 인증 응답 타입 및 저장 로직을 수정했습니다. 프로필 화면에 userId 기반 조회와 OTT 바텀시트(사이드이펙트) 추가, 컬렉션 리스트에 userId 전달·북마크 디바운스 로직이 도입되었습니다. Changes
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 바텀시트 표시
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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 저장 로직으로 대체
|
|
||
| val userId = savedStateHandle.toRoute<Route.Profile>().userId |
There was a problem hiding this comment.
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.Profile과 Route.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.
| 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.
There was a problem hiding this comment.
확인해봤는데 userId null로 들어가서 괜찮던데?
There was a problem hiding this comment.
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()실패 시 에러 처리가 누락되었습니다.
suspendRunCatching의onFailure블록이 없어 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을 발생시킵니다.
There was a problem hiding this comment.
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") - }
| fun saveTempUserData() = viewModelScope.launch { | ||
| preferencesManager.saveString(ACCESS_TOKEN, "eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOjgwMDAyMTcxMTM1NzExODE5MSwicm9sZSI6IkZMSU5HIiwidHlwZSI6IkFDQ0VTUyIsImlhdCI6MTc2ODgzODU0MCwiZXhwIjoxNzcwMDQ4MTQwfQ.A2XYyu24IoGAXNSQHJ1S-iudWmg8II2_ivI4EdyyWw9KS9oJlxHKOAhcKrsLpLkc9kllZyxwaTJO1t4vI7oZlg") | ||
| preferencesManager.saveString(USER_ID, "800021711357118191") | ||
| preferencesManager.saveString(USER_NAME, "hojoo") | ||
| } |
There was a problem hiding this comment.
🚨 보안 취약점: 하드코딩된 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.
| ) : ViewModel() { | ||
|
|
||
| val routeType = savedStateHandle.toRoute<Route.CollectionList>().routeType | ||
| val routeReceiveData = savedStateHandle.toRoute<Route.CollectionList>() |
There was a problem hiding this comment.
p3: 화면에 표시할 데이터가 담겨있는 것 같아요. uiState로 한 번에 관리할 수 있나요? 👀
There was a problem hiding this comment.
uiState로 관리해줘야 하는 데이터일까? 싶어서 따로 설정해줬던 건데, 수정하는 편이 좋을 것 같네요ㅎㅎ 감사합니다
kimjw2003
left a comment
There was a problem hiding this comment.
🔍 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를 확인해주세요.
🔴 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 }
}문제점:
수정 제안: // ✅ 수정 코드
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
} |
🔴 Critical Issue #2: runBlocking in Coroutine Context (UserRepository.kt)// ❌ 현재 코드
private fun myUserId(): String = runBlocking {
preferencesManager.getString(USER_ID).first()
}문제점:
수정 제안: // ✅ 수정 코드
private suspend fun myUserId(): String {
return preferencesManager.getString(USER_ID).first()
}이 함수를 호출하는 모든 곳도 suspend context에서 호출하도록 수정해주세요. |
|
✅ 잘 구현된 부분
머지 권장사항: Critical Issues (1, 2번)를 먼저 수정하고, 테스트 토큰을 제거한 후 머지하는 것을 권장합니다. 🚀 |
There was a problem hiding this comment.
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: 두 헬퍼 함수의 중복 로직을 통합할 수 있습니다.
updateCollectionBookmarkState와updateCollectionIsBookmarkedOnly가 유사한 구조를 가지고 있습니다. 선택적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) - ) - ) - } - }
| // 사용자별 북마크한 콘텐츠 목록 조회 | ||
| suspend fun getUserBookmarkedContents(userId: String?) : Result<BookmarkedContentListModel> = | ||
| suspendRunCatching { apiService.getBookmarkedContentListByUserId(userId ?: myUserId()).data.toModel() } |
There was a problem hiding this comment.
🧩 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 1Repository: 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 1Repository: 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.
📮 관련 이슈
📌 작업 내용
📸 스크린샷
Screen_recording_20260122_163120.mp4
Screen_recording_20260122_175248.mp4
🫛 To. 리뷰어
Summary by CodeRabbit
릴리스 노트
새 기능
개선 사항
기타
✏️ Tip: You can customize this high-level summary in your review settings.