[Feat] Profile 프로필 관련 API 연동#145
Conversation
- `Brush.linearGradient` 대신 `ShaderBrush`와 `LinearGradientShader`를 사용하여 정교한 그라데이션 좌표(Offset) 적용 - gradient900, gradient700, gradient400 색상에 대한 그라데이션 시작/끝 지점 커스텀 구현
… feat/#134-profilescreen-logic-and-api # Conflicts: # app/src/main/java/com/flint/core/designsystem/component/listItem/CollectionItem.kt # app/src/main/java/com/flint/core/designsystem/component/listItem/SavedContentItem.kt # app/src/main/java/com/flint/core/designsystem/component/listView/SavedContentsSection.kt # app/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.kt # app/src/main/java/com/flint/domain/mapper/collection/CollectionMapper.kt # app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt # app/src/main/java/com/flint/domain/repository/CollectionRepository.kt # app/src/main/java/com/flint/domain/repository/ContentRepository.kt # app/src/main/java/com/flint/domain/repository/HomeRepository.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/profile/ProfileUiState.kt
…creen-logic-and-api
📝 WalkthroughWalkthrough프로필 조회 API(getUserProfile)와 DTO/모델/매퍼 추가, UserRepository와 ProfileViewModel에 프로필 로드 로직 추가, ProfileScreen·ProfileKeywordSection·ProfileUiState·디자인 시스템 색상(gradient) 및 일부 UI 수식 변경을 포함한 리팩토링입니다. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as ProfileScreen (UI)
participant VM as ProfileViewModel (ViewModel)
participant Repo as UserRepository (Repository)
participant API as UserApi (API)
participant Server as Backend
UI->>VM: getProfile()
activate VM
VM->>Repo: getUserProfile(userId)
VM->>Repo: getUserKeywords(userId)
activate Repo
par Parallel Fetch
Repo->>API: getUserProfile()
activate API
API->>Server: GET /api/v1/users/{userId}
Server-->>API: UserProfileResponseDto
API-->>Repo: BaseResponse<UserProfileResponseDto>
deactivate API
and
Repo->>API: getUserKeywords()
activate API
API->>Server: GET /api/v1/users/{userId}/keywords
Server-->>API: UserKeywordResponseDto[]
API-->>Repo: BaseResponse<List<...>>
deactivate API
end
Repo-->>VM: Result<UserProfileResponseModel>, Result<List<UserKeywordResponseModel>>
deactivate Repo
VM->>UI: emit UiState.Success(ProfileUiState)
deactivate VM
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
시 🐰
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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: 2
🤖 Fix all issues with AI agents
In `@app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt`:
- Line 39: Remove the stray unused construction Pair(profileResult.getOrThrow(),
keywordsResult.getOrThrow()) and instead use the already-evaluated results when
building/returning the combined value in ProfileViewModel (e.g., reuse the local
variables or assign profileResult.getOrThrow() and keywordsResult.getOrThrow()
to locals once and create the Pair from those locals); update the code paths
that currently call getOrThrow() again (lines using profileResult and
keywordsResult) to reference the stored values so getOrThrow() is invoked only
where the result is actually used.
- Around line 49-52: 현재 ProfileViewModel의 흐름에서 }.onSuccess { combinedState ->
_uiState.update { UiState.Success(combinedState) } } 부분에 onFailure 처리가 빠져 있어 API
오류 시 상태가 업데이트되지 않습니다; 해당 체인(ProfileViewModel 내의 해당 flow 콜) 바로 뒤에 onFailure {
throwable -> _uiState.update { UiState.Error(/* 적절한 오류 메시지 또는 throwable */) } }
를 추가해 실패 케이스를 UiState의 에러 상태로 전파하고, 필요하면 throwable을 사용자용 문자열로 변환하거나 로그에 남기도록
처리하세요.
🧹 Nitpick comments (3)
app/src/main/java/com/flint/domain/model/user/UserKeywordResponseModel.kt (1)
66-84: FakeList2/3의 인덱스 의존을 방어적으로 처리하는 편이 안전합니다.FakeList1의 항목 수나 순서가 바뀌면 초기화 시 IndexOutOfBounds가 날 수 있어요. 최소한
getOrNull기반으로 구성하거나take(6)로 전제 조건을 명시해 주세요.🔧 예시 변경안
- val FakeList2: ImmutableList<UserKeywordResponseModel> = - persistentListOf( - FakeList1[0], - FakeList1[1], - FakeList1[2], - FakeList1[3].copy(name = "키워드"), - FakeList1[4].copy(name = "설레는"), - FakeList1[5].copy(name = "키워드"), - ) + val FakeList2: ImmutableList<UserKeywordResponseModel> = + listOfNotNull( + FakeList1.getOrNull(0), + FakeList1.getOrNull(1), + FakeList1.getOrNull(2), + FakeList1.getOrNull(3)?.copy(name = "키워드"), + FakeList1.getOrNull(4)?.copy(name = "설레는"), + FakeList1.getOrNull(5)?.copy(name = "키워드"), + ).toPersistentList()app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt (2)
71-95: ProfileRefreshButton이 현재 미사용입니다.리프레시 UI가 의도된 기능이면 헤더에 배치하고, 아니라면 버튼과 onRefreshClick 파라미터를 정리해 주세요.
158-181: 키워드 6개 초과 입력 시 처리 의도를 명시하는 게 좋습니다.현재 로직은 6개 초과 시 상위 6개만 사실상 사용됩니다. 의도가 맞다면
take(6)로 가정을 명시해 주세요.🔧 예시 변경안
- val sortedByRank = keywordList.sortedBy { it.rank } + val sortedByRank = keywordList.sortedBy { it.rank }.take(6)
| // 프로필/컬렉션/작품 목록 등 설정 필요한 초기 데이터 로드 필요 | ||
| } | ||
| } | ||
| Pair(profileResult.getOrThrow(), keywordsResult.getOrThrow()) |
There was a problem hiding this comment.
사용되지 않는 코드 제거 필요
Pair(profileResult.getOrThrow(), keywordsResult.getOrThrow()) 결과가 변수에 할당되지 않고 버려지고 있습니다. 이 줄은 불필요하며, 라인 42-43에서 동일한 getOrThrow() 호출이 다시 수행됩니다.
🐛 수정 제안
val profileResult = profileDeferred.await()
val keywordsResult = keywordsDeferred.await()
- Pair(profileResult.getOrThrow(), keywordsResult.getOrThrow())
-
ProfileUiState(
profile = profileResult.getOrThrow(),📝 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.
| Pair(profileResult.getOrThrow(), keywordsResult.getOrThrow()) | |
| val profileResult = profileDeferred.await() | |
| val keywordsResult = keywordsDeferred.await() | |
| ProfileUiState( | |
| profile = profileResult.getOrThrow(), |
🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt` at line
39, Remove the stray unused construction Pair(profileResult.getOrThrow(),
keywordsResult.getOrThrow()) and instead use the already-evaluated results when
building/returning the combined value in ProfileViewModel (e.g., reuse the local
variables or assign profileResult.getOrThrow() and keywordsResult.getOrThrow()
to locals once and create the Pair from those locals); update the code paths
that currently call getOrThrow() again (lines using profileResult and
keywordsResult) to reference the stored values so getOrThrow() is invoked only
where the result is actually used.
| }.onSuccess { combinedState -> | ||
| _uiState.update { UiState.Success(combinedState) } | ||
| } | ||
| } |
There was a problem hiding this comment.
에러 처리 누락
onFailure 처리가 없어서 API 호출 실패 시 사용자에게 에러 피드백이 전달되지 않고 UiState.Empty 상태로 유지됩니다. PR 목표에 로딩 상태 관리가 남은 작업으로 명시되어 있지만, 에러 상태 처리도 함께 추가하는 것이 좋습니다.
🐛 에러 처리 추가 제안
}.onSuccess { combinedState ->
_uiState.update { UiState.Success(combinedState) }
+ }.onFailure { exception ->
+ _uiState.update { UiState.Error(exception) }
}🤖 Prompt for AI Agents
In `@app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt` around
lines 49 - 52, 현재 ProfileViewModel의 흐름에서 }.onSuccess { combinedState ->
_uiState.update { UiState.Success(combinedState) } } 부분에 onFailure 처리가 빠져 있어 API
오류 시 상태가 업데이트되지 않습니다; 해당 체인(ProfileViewModel 내의 해당 flow 콜) 바로 뒤에 onFailure {
throwable -> _uiState.update { UiState.Error(/* 적절한 오류 메시지 또는 throwable */) } }
를 추가해 실패 케이스를 UiState의 에러 상태로 전파하고, 필요하면 throwable을 사용자용 문자열로 변환하거나 로그에 남기도록
처리하세요.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt`:
- Around line 73-97: ProfileRefreshButton is implemented but never used in
ProfileKeywordSection, so the onRefreshClick handler is never invoked; open the
ProfileKeywordSection composable and add a call to ProfileRefreshButton (passing
the existing onRefreshClick lambda) into the top Row (the section header Row
that contains the title/actions) so the refresh control is visible and
interactive; ensure you import/use the same modifier and alignment conventions
as the Row and remove any conflicting click handlers so ProfileRefreshButton
receives clicks.
- Around line 160-183: arrangeKeywordsByRank currently discards keywords beyond
the first 6; preserve extras by applying the special 6-item reordering only to
the top-six and appending any remaining items after that. Concretely, inside
arrangeKeywordsByRank use sortedByRank.take(6) (or similar) to build the
reordered block using threeRowOrder/twoRowOrder, then append
sortedByRank.drop(6) to the resulting list before converting toPersistentList
and returning; ensure shouldUseThreeRows is still computed from the top-six
(e.g., checkTargets from the taken list) and keep the existing row-count return
value logic or make it dynamic as needed.
| private fun ProfileRefreshButton( | ||
| onRefreshClick: () -> Unit, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| Box( | ||
| contentAlignment = Alignment.Center, | ||
| Column( | ||
| verticalArrangement = Arrangement.spacedBy(4.dp), | ||
| horizontalAlignment = Alignment.CenterHorizontally, | ||
| modifier = | ||
| modifier | ||
| .fillMaxWidth(), | ||
| .noRippleClickable( | ||
| onClick = onRefreshClick, | ||
| ), | ||
| ) { | ||
| FlowRow( | ||
| horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), | ||
| verticalArrangement = Arrangement.spacedBy(12.dp), | ||
| itemVerticalAlignment = Alignment.CenterVertically, | ||
| maxItemsInEachRow = 3, | ||
| modifier = | ||
| Modifier | ||
| .align(Alignment.Center), | ||
| ) { | ||
| keywordList.forEach { | ||
| with(it) { | ||
| Icon( | ||
| imageVector = ImageVector.vectorResource(R.drawable.ic_refresh), | ||
| contentDescription = null, | ||
| tint = FlintTheme.colors.secondary400, | ||
| ) | ||
| Text( | ||
| text = "업데이트", | ||
| style = FlintTheme.typography.micro1M10, | ||
| color = FlintTheme.colors.gray100, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
리프레시 버튼이 실제 UI에 연결되지 않아 동작이 막힙니다.
ProfileRefreshButton가 정의됐지만 ProfileKeywordSection에서 호출되지 않아 onRefreshClick이 죽어 있습니다. 의도된 기능이면 상단 Row에 배치해 동작을 복원하세요.
🛠️ 연결 예시
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth(),
) {
Column {
Text(
text = "${nickname}님의 취향키워드",
style = FlintTheme.typography.head3Sb18,
color = FlintTheme.colors.white,
)
Spacer(Modifier.height(4.dp))
Text(
text = "${nickname}님이 관심있어하는 키워드에요",
style = FlintTheme.typography.body2R14,
color = FlintTheme.colors.gray100,
)
}
+ ProfileRefreshButton(
+ onRefreshClick = onRefreshClick,
+ )
}🤖 Prompt for AI Agents
In
`@app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt`
around lines 73 - 97, ProfileRefreshButton is implemented but never used in
ProfileKeywordSection, so the onRefreshClick handler is never invoked; open the
ProfileKeywordSection composable and add a call to ProfileRefreshButton (passing
the existing onRefreshClick lambda) into the top Row (the section header Row
that contains the title/actions) so the refresh control is visible and
interactive; ensure you import/use the same modifier and alignment conventions
as the Row and remove any conflicting click handlers so ProfileRefreshButton
receives clicks.
| private fun arrangeKeywordsByRank( | ||
| keywordList: ImmutableList<UserKeywordResponseModel> | ||
| ): Pair<ImmutableList<UserKeywordResponseModel>, Int> { | ||
| if (keywordList.size < 6) return keywordList to 3 | ||
|
|
||
| val sortedByRank = keywordList.sortedBy { it.rank } | ||
|
|
||
| // rank 1, 2, 3 (상위 3개) → Large | ||
| val topRanks = sortedByRank.take(3) | ||
| // rank 4, 5, 6 (하위 3개) → Small | ||
| val bottomRanks = sortedByRank.drop(3) | ||
| val checkTargets = listOf( | ||
| sortedByRank[0], // rank 1 | ||
| sortedByRank[1], // rank 2 | ||
| sortedByRank[3], // rank 4 | ||
| ) | ||
|
|
||
| // 번갈아 배치: 1, 4, 2, 5, 3, 6 | ||
| return topRanks | ||
| .zip(bottomRanks) | ||
| .flatMap { (top, bottom) -> listOf(top, bottom) } | ||
| .toPersistentList() | ||
| val shouldUseThreeRows = checkTargets.any { it.name.length >= 3 } | ||
|
|
||
| return if (shouldUseThreeRows) { | ||
| // 3줄 배치: [1, 4], [5, 2], [3, 6] | ||
| val threeRowOrder = listOf(0, 3, 4, 1, 2, 5) | ||
| threeRowOrder.map { sortedByRank[it] }.toPersistentList() to 2 | ||
| } else { | ||
| // 2줄 배치: [1, 4, 2], [5, 3, 6] | ||
| val twoRowOrder = listOf(0, 3, 1, 4, 2, 5) | ||
| twoRowOrder.map { sortedByRank[it] }.toPersistentList() to 3 | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the file structure and find related code
cd app/src/main/java/com/flint/presentation/profile/component && pwdRepository: imflint/Flint-Android
Length of output: 142
🏁 Script executed:
# Search for arrangeKeywordsByRank function and its usages
rg "arrangeKeywordsByRank" --type kt -B 2 -A 10Repository: imflint/Flint-Android
Length of output: 91
🏁 Script executed:
# Check the UserKeywordResponseModel to understand the data structure
fd "UserKeywordResponseModel" --type kt --exec cat {} \;Repository: imflint/Flint-Android
Length of output: 234
🏁 Script executed:
# Look for API response models and constraints
rg "UserKeyword" --type kt -l | head -20Repository: imflint/Flint-Android
Length of output: 91
🏁 Script executed:
# Search for arrangeKeywordsByRank function and its usages
rg "arrangeKeywordsByRank" -B 2 -A 10Repository: imflint/Flint-Android
Length of output: 3031
🏁 Script executed:
# Look for UserKeywordResponseModel definition
rg "UserKeywordResponseModel" -B 2 -A 10Repository: imflint/Flint-Android
Length of output: 26127
🏁 Script executed:
# Find the ProfileKeywordSection file and check the full context
cat -n app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.ktRepository: imflint/Flint-Android
Length of output: 8766
🏁 Script executed:
# Search for API calls or data sources that provide keyword data
rg "keywordList" app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt -B 5 -A 5Repository: imflint/Flint-Android
Length of output: 2502
🏁 Script executed:
# Check API response DTO to see if there's a 6-keyword limit
find . -name "*UserKeywords*Dto*" -o -name "*KeywordDto*" | head -20Repository: imflint/Flint-Android
Length of output: 144
🏁 Script executed:
# Look for API interface to understand the contract
rg "UserApi" -A 10 --type-list | head -50Repository: imflint/Flint-Android
Length of output: 454
🏁 Script executed:
# Search for any configuration or limits related to keywords
rg "limit|MAX.*KEYWORD|KEYWORD.*MAX|6" app/src/main/java/com/flint/data -i | grep -i keyword | head -20Repository: imflint/Flint-Android
Length of output: 47
🏁 Script executed:
# Check the API interface definition
fd "UserApi" -type f --exec cat {} \;Repository: imflint/Flint-Android
Length of output: 235
🏁 Script executed:
# Read the UserKeywordsResponseDto to understand the API structure
cat -n app/src/main/java/com/flint/data/dto/user/response/UserKeywordsResponseDto.ktRepository: imflint/Flint-Android
Length of output: 783
🏁 Script executed:
# Find and read the UserApi interface
find . -name "UserApi.kt" -exec cat -n {} \;Repository: imflint/Flint-Android
Length of output: 977
🏁 Script executed:
# Search for any configuration constants or limits
rg "keyword" app/src/main/java/com/flint --ignore-case -B 2 -A 2 | grep -i "limit\|max\|count\|size" | head -30Repository: imflint/Flint-Android
Length of output: 1015
🏁 Script executed:
# Check if there's any documentation or comments about 6 keywords
rg "6|keyword" app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt -B 1 -A 1 | head -50Repository: imflint/Flint-Android
Length of output: 1754
🏁 Script executed:
# Search for any comments or documentation about 6 keywords
rg "3줄|2줄|배치|줄|row|grid" app/src/main/java/com/flint/presentation/profile -B 5 -A 5 | head -60Repository: imflint/Flint-Android
Length of output: 5696
🏁 Script executed:
# Check if there are other uses of this function or similar logic elsewhere
rg "arrangeKeyword|chunked|itemsPerRow" app/src/main/java/com/flint --type-listRepository: imflint/Flint-Android
Length of output: 454
🏁 Script executed:
# More precise search
find . -name "*.kt" -path "*/profile/*" -exec grep -l "keyword" {} \;Repository: imflint/Flint-Android
Length of output: 537
6개 초과의 키워드가 자동으로 버려집니다.
현재 arrangeKeywordsByRank 함수는 6개 이상의 키워드가 입력되면 상위 6개만 재배치하고 나머지를 버립니다. API 응답(UserKeywordsResponseDto)에는 키워드 개수 제한이 없으며, 실제로 6개를 초과할 수 있습니다. 데이터 손실이 명시적으로 문서화되지 않았으며, 사용자는 일부 키워드가 무시되는 이유를 알 수 없습니다.
개선 방법:
- 6개 제한이 API 계약이라면 명시적으로 문서화하기
- 또는 초과분을 그리드 아래에 추가로 표시하기
- 또는 명시적으로 제한하되
take(6)전에 사용자에게 알리기
🤖 Prompt for AI Agents
In
`@app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt`
around lines 160 - 183, arrangeKeywordsByRank currently discards keywords beyond
the first 6; preserve extras by applying the special 6-item reordering only to
the top-six and appending any remaining items after that. Concretely, inside
arrangeKeywordsByRank use sortedByRank.take(6) (or similar) to build the
reordered block using threeRowOrder/twoRowOrder, then append
sortedByRank.drop(6) to the resulting list before converting toPersistentList
and returning; ensure shouldUseThreeRows is still computed from the top-six
(e.g., checkTargets from the taken list) and keep the existing row-count return
value logic or make it dynamic as needed.
There was a problem hiding this comment.
@nahy-512, 알겠습니다! 6개 초과 시 버리는 것이 의도된 동작이라는 점 확인했습니다. 👍
✏️ Learnings added
Learnt from: nahy-512
Repo: imflint/Flint-Android PR: 145
File: app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt:160-183
Timestamp: 2026-01-20T19:26:26.529Z
Learning: In ProfileKeywordSection.kt's arrangeKeywordsByRank function, discarding keywords beyond 6 items is the intended behavior per design requirements. The function should only process and display the top 6 keywords.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| val profileDeferred = async { userRepository.getUserProfile(userId = null) } | ||
| val keywordsDeferred = async { userRepository.getUserKeywords(userId = null) } | ||
|
|
||
| private fun loadInitialData() { | ||
| viewModelScope.launch { | ||
| _uiState.emit(UiState.Success(ProfileUiState.Fake)) // TODO: 임시 로직 | ||
| val profileResult = profileDeferred.await() | ||
| val keywordsResult = keywordsDeferred.await() |
There was a problem hiding this comment.
p3: userRepository.getUserProfile, userRepository.getUserKeywords가 이미 suspend 함수이므로, async와 await()을 굳이 사용할 필요가 없어 보입니다.
그냥 val profile = userRepository.getUserProfile(userId = null).getOrThrow로 사용하면 되지 않을까요?
There was a problem hiding this comment.
다음 작업에서 확인해보겠습니다! 감사합니다
📮 관련 이슈
📌 작업 내용
📸 스크린샷
[취향 키워드]

😅 미구현
🫛 To. 리뷰어
Summary by CodeRabbit
새로운 기능
개선사항
✏️ Tip: You can customize this high-level summary in your review settings.