[Feature] 마이페이지 api 연결#211
Hidden character warning
Conversation
|
Warning Review limit reached
More reviews will be available in 46 minutes and 48 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 Walkthrough워크스루PR은 북마크 콘텐츠 목록 조회에 커서 기반 페이지네이션을 도입하고, 변경 사항북마크 목록 및 키워드 재계산 기능
예상 코드 리뷰 난이도🎯 4 (Complex) | ⏱️ ~60분 관련 PR들
제안 라벨
제안 리뷰어
시 🐰
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/flint/data/dto/user/response/UserProfileResponseDto.kt (1)
1-18:⚠️ Potential issue | 🟡 Minor
UserProfileResponseDto직렬화 애노테이션 불일치 수정 필요
NetworkModule에서 RetrofitaddConverterFactory는kotlinx.serialization(asConverterFactory)만 사용 중입니다.- 그런데
UserProfileResponseDto는@Serializable(kotlinx)와@SerializedName(Gson)을 함께 쓰고 있어, Gson@SerializedName은 kotlinx 직렬화에서는 적용되지 않습니다.BookmarkedContentListResponseDto를 포함한 대부분의 DTO가kotlinx.serialization.SerialName을 쓰므로,UserProfileResponseDto도@SerializedName을@SerialName으로 바꾸거나 Gson 애노테이션/임포트를 제거해 일관성을 맞추세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/com/flint/data/dto/user/response/UserProfileResponseDto.kt` around lines 1 - 18, UserProfileResponseDto mixes Gson (`@SerializedName`) with kotlinx.serialization (`@Serializable`) while NetworkModule uses Retrofit with kotlinx.asConverterFactory; replace all `@SerializedName` usages in UserProfileResponseDto with kotlinx.serialization.SerialName (or remove Gson imports) so field names are honored by kotlinx serialization, e.g., update the annotations on id, profileImageUrl, isFliner, nickname, keywordRecalculatable to `@SerialName` and remove com.google.gson imports to match the rest of DTOs and NetworkModule’s asConverterFactory usage.
🧹 Nitpick comments (3)
app/src/main/java/com/flint/presentation/savedcontent/SavedContentListScreen.kt (1)
5-16: 💤 Low valuesavedcontent 패키지에서 profile 패키지로의 크로스 패키지 의존성
SavedContentListRoute가profile.SavedContentRoute로 단순 위임하면서 savedcontent → profile 방향의 프레젠테이션 레이어 간 의존성이 생성되었습니다. 이는 SavedContentRoute가 profile 패키지로 통합된 의도적인 리팩토링으로 보이지만, 패키지 간 결합도가 증가하는 점을 유의하시기 바랍니다.만약 향후 savedcontent와 profile 기능을 독립적으로 유지하려는 경우, SavedContentRoute의 위치를 재고하거나 공통 모듈로 추출하는 것을 검토해보세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/com/flint/presentation/savedcontent/SavedContentListScreen.kt` around lines 5 - 16, SavedContentListRoute currently delegates to profile.SavedContentRoute, introducing a cross-package dependency from savedcontent to profile; either move SavedContentRoute into the savedcontent package, extract it into a shared/common module, or create a local SavedContentRoute implementation/adapter in savedcontent that wraps the profile implementation to avoid direct package coupling—update references to use the chosen location and ensure the function signature (SavedContentListRoute, SavedContentRoute, paddingValues, navigateUp) remains consistent.app/src/main/java/com/flint/presentation/profile/SavedContentViewModel.kt (1)
85-141: ⚖️ Poor tradeoff수동 JSON 파싱의 취약성
북마크 토글 실패 시 에러 응답을 수동으로 JSON 파싱하여
BOOKMARK.CONTENT_MIN_LIMIT에러 코드를 확인하고 있습니다(124-132줄). 이 접근 방식은 다음과 같은 문제가 있습니다:
- 에러 응답 구조 변경 시 파싱 실패 가능
- JSON 파싱 예외 처리가
runCatching으로만 되어 있어 실패 원인 추적 어려움- 타입 안정성 부재
다만 PR 목표에 따르면 해당 비즈니스 에러를 글로벌 네트워크 에러 처리에서 제외하기 위한 의도적인 설계로 보입니다.
장기적으로는 에러 응답을 별도의 DTO로 정의하고
Converter를 통해 파싱하는 것을 권장합니다:data class ErrorResponse( val errorCode: String, val message: String? ) // Retrofit Converter 또는 별도 파싱 유틸리티 활용현재는 PR 목표에 부합하므로 승인하되, 향후 리팩토링 시 고려해주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/com/flint/presentation/profile/SavedContentViewModel.kt` around lines 85 - 141, The toggleBookmark failure handling in SavedContentViewModel currently does fragile manual JSON parsing to detect BOOKMARK.CONTENT_MIN_LIMIT; replace this with a typed ErrorResponse DTO and use Retrofit's converter (or a shared parsing utility) to convert throwable.response().errorBody() into ErrorResponse, then check errorResponse.errorCode == "BOOKMARK.CONTENT_MIN_LIMIT"; update the onFailure block (where isMinLimitError is computed) to use the converter and proper try/catch that logs parsing errors, and then call _uiState.update/showBookmarkRestrictionModal or Timber.e accordingly.app/src/main/java/com/flint/presentation/profile/SavedContentScreen.kt (1)
209-273: ⚡ Quick win프리뷰 데이터에 북마크 상태 추가를 권장합니다.
SavedContentPreviewData.FakeList의BookmarkedContentItemModel인스턴스들이isBookmarked와bookmarkCount속성을 설정하지 않고 있습니다. 이제 실제 화면에서 이 속성들을 사용하므로(lines 196-197), 프리뷰에서도 현실적인 북마크 상태를 보여주도록 데이터를 업데이트하는 것이 좋습니다.♻️ 프리뷰 데이터 개선 제안
BookmarkedContentItemModel( id = "0", title = "은하수를 여행하는 히치하이커를 위한 안내서", year = 2005, imageUrl = "", getOttSimpleList = listOf( OttType.Netflix, OttType.Disney, OttType.Tving, ), + isBookmarked = true, + bookmarkCount = 42, ),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/com/flint/presentation/profile/SavedContentScreen.kt` around lines 209 - 273, SavedContentPreviewData.FakeList contains BookmarkedContentItemModel entries missing isBookmarked and bookmarkCount; update each BookmarkedContentItemModel in SavedContentScreen.kt (FakeList) to set realistic isBookmarked (true/false) and bookmarkCount integers so previews reflect actual UI usage (see usages at lines referencing isBookmarked and bookmarkCount). Locate the FakeList declaration and add the isBookmarked and bookmarkCount properties to each BookmarkedContentItemModel constructor (vary values across items to show different states: bookmarked vs not and different counts).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/java/com/flint/data/di/interceptor/NetworkErrorInterceptor.kt`:
- Around line 29-31: The current detection uses
response.peekBody(Long.MAX_VALUE).string().contains("\"errorCode\"") which risks
OOM and false positives; change NetworkErrorInterceptor to call
response.peekBody with a bounded buffer (e.g., 8KB–64KB) instead of
Long.MAX_VALUE and replace the naive contains check with a proper JSON field
existence test (use your project's JSON parser or JSONObject to parse the peeked
body and check for the "errorCode" key) when computing isBusinessError so large
bodies are not fully buffered and only a parsed JSON check determines presence
of the errorCode field.
In `@app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt`:
- Around line 23-28: The current MyBookmarkedContentListResponseDto.toModel()
sets totalCount = data.size which only reflects items in the current cursor
page; change it to use the DTO's meta.returned value when available (e.g.,
totalCount = meta?.returned ?: data.size) so the model's totalCount reflects the
DTO's intended count metric, and leave UserRepository's aggregation logic (which
builds BookmarkedContentListModel(totalCount = allContents.size)) to override as
needed; update the MyBookmarkedContentListResponseDto.toModel() implementation
accordingly.
In `@app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt`:
- Around line 26-40: The current use of Result.onSuccess in
toggleCollectionBookmark and toggleContentBookmark attempts to call the suspend
function _bookmarkChanges.emit(...) from a non-suspending lambda
(Result.onSuccess), causing a compile error; fix by replacing the onSuccess
usage with a suspend-friendly check such as val isBookmarked =
result.getOrNull(); if (isBookmarked != null) {
_bookmarkChanges.emit(BookmarkChange.Collection(collectionId, isBookmarked)) }
(and similarly for BookmarkChange.Content in toggleContentBookmark) so the emit
call runs directly in the surrounding suspend function.
In `@app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt`:
- Around line 64-70: 현재 코드에서 savedContents.contents를 필터한 뒤 totalCount를
filtered.size로 덮어써 전체 개수 계약이 깨집니다; 대신 북마크 해제 로직(예: where change.id is
removed)에서는 contents를 filtered로 갱신하되 savedContents.totalCount는 기존 totalCount에서
1만 감소시키는 방식으로 유지하세요 (즉, data.savedContents.copy(contents = filtered, totalCount
= maxOf(data.savedContents.totalCount - 1, 0))처럼 기존 totalCount를 직접 조작),
savedContents와 화면 리스트 길이를 분리해서 다루도록 ProfileViewModel의 해당 분기(데이터 업데이트:
data.savedContents.contents, totalCount)를 수정하십시오.
---
Outside diff comments:
In
`@app/src/main/java/com/flint/data/dto/user/response/UserProfileResponseDto.kt`:
- Around line 1-18: UserProfileResponseDto mixes Gson (`@SerializedName`) with
kotlinx.serialization (`@Serializable`) while NetworkModule uses Retrofit with
kotlinx.asConverterFactory; replace all `@SerializedName` usages in
UserProfileResponseDto with kotlinx.serialization.SerialName (or remove Gson
imports) so field names are honored by kotlinx serialization, e.g., update the
annotations on id, profileImageUrl, isFliner, nickname, keywordRecalculatable to
`@SerialName` and remove com.google.gson imports to match the rest of DTOs and
NetworkModule’s asConverterFactory usage.
---
Nitpick comments:
In `@app/src/main/java/com/flint/presentation/profile/SavedContentScreen.kt`:
- Around line 209-273: SavedContentPreviewData.FakeList contains
BookmarkedContentItemModel entries missing isBookmarked and bookmarkCount;
update each BookmarkedContentItemModel in SavedContentScreen.kt (FakeList) to
set realistic isBookmarked (true/false) and bookmarkCount integers so previews
reflect actual UI usage (see usages at lines referencing isBookmarked and
bookmarkCount). Locate the FakeList declaration and add the isBookmarked and
bookmarkCount properties to each BookmarkedContentItemModel constructor (vary
values across items to show different states: bookmarked vs not and different
counts).
In `@app/src/main/java/com/flint/presentation/profile/SavedContentViewModel.kt`:
- Around line 85-141: The toggleBookmark failure handling in
SavedContentViewModel currently does fragile manual JSON parsing to detect
BOOKMARK.CONTENT_MIN_LIMIT; replace this with a typed ErrorResponse DTO and use
Retrofit's converter (or a shared parsing utility) to convert
throwable.response().errorBody() into ErrorResponse, then check
errorResponse.errorCode == "BOOKMARK.CONTENT_MIN_LIMIT"; update the onFailure
block (where isMinLimitError is computed) to use the converter and proper
try/catch that logs parsing errors, and then call
_uiState.update/showBookmarkRestrictionModal or Timber.e accordingly.
In
`@app/src/main/java/com/flint/presentation/savedcontent/SavedContentListScreen.kt`:
- Around line 5-16: SavedContentListRoute currently delegates to
profile.SavedContentRoute, introducing a cross-package dependency from
savedcontent to profile; either move SavedContentRoute into the savedcontent
package, extract it into a shared/common module, or create a local
SavedContentRoute implementation/adapter in savedcontent that wraps the profile
implementation to avoid direct package coupling—update references to use the
chosen location and ensure the function signature (SavedContentListRoute,
SavedContentRoute, paddingValues, navigateUp) remains consistent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9c09fff1-2fe7-4eed-b808-61be63be29f2
📒 Files selected for processing (25)
app/src/main/java/com/flint/core/navigation/Route.ktapp/src/main/java/com/flint/data/api/ContentApi.ktapp/src/main/java/com/flint/data/api/UserApi.ktapp/src/main/java/com/flint/data/di/interceptor/NetworkErrorInterceptor.ktapp/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.ktapp/src/main/java/com/flint/data/dto/user/response/UserProfileResponseDto.ktapp/src/main/java/com/flint/domain/mapper/content/ContentMapper.ktapp/src/main/java/com/flint/domain/mapper/user/ProfileMapper.ktapp/src/main/java/com/flint/domain/model/bookmark/BookmarkChange.ktapp/src/main/java/com/flint/domain/model/content/BookmarkedContentListModel.ktapp/src/main/java/com/flint/domain/model/user/UserProfileResponseModel.ktapp/src/main/java/com/flint/domain/repository/BookmarkRepository.ktapp/src/main/java/com/flint/domain/repository/UserRepository.ktapp/src/main/java/com/flint/presentation/main/MainNavHost.ktapp/src/main/java/com/flint/presentation/main/MainNavigator.ktapp/src/main/java/com/flint/presentation/profile/ProfileScreen.ktapp/src/main/java/com/flint/presentation/profile/ProfileViewModel.ktapp/src/main/java/com/flint/presentation/profile/SavedContentScreen.ktapp/src/main/java/com/flint/presentation/profile/SavedContentViewModel.ktapp/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.ktapp/src/main/java/com/flint/presentation/profile/navigation/ProfileNavigation.ktapp/src/main/java/com/flint/presentation/profile/uistate/ProfileUiState.ktapp/src/main/java/com/flint/presentation/profile/uistate/SavedContentUiState.ktapp/src/main/java/com/flint/presentation/savedcontent/SavedContentListScreen.ktapp/src/main/java/com/flint/presentation/savedcontent/navigation/SavedContentNavigation.kt
| val isBusinessError = response.peekBody(Long.MAX_VALUE) | ||
| .string() | ||
| .contains("\"errorCode\"") |
There was a problem hiding this comment.
peekBody 버퍼 크기를 제한하세요.
Long.MAX_VALUE를 버퍼 크기로 사용하면 대용량 응답 시 메모리 부족(OOM) 위험이 있습니다. 실무에서는 에러 응답 바디가 보통 작으므로 8KB~64KB 정도로 제한하는 것이 안전합니다.
또한, contains("\"errorCode\"")로 문자열 기반 감지는 취약합니다. errorCode가 메시지나 다른 필드 값에 포함될 경우 오탐이 발생할 수 있습니다(예: {"message": "Missing errorCode field"}). JSON 파싱이나 정규식을 사용해 errorCode 필드의 실제 존재 여부를 확인하는 것이 더 정확합니다.
🛡️ 개선 제안
- val isBusinessError = response.peekBody(Long.MAX_VALUE)
+ val bufferSize = 8192L // 8KB
+ val isBusinessError = response.peekBody(bufferSize)
.string()
- .contains("\"errorCode\"")
+ .let { body ->
+ // JSON 필드로 정확히 감지
+ Regex(""""errorCode"\s*:""").containsMatchIn(body)
+ }또는 프로젝트에서 JSON 파서(Gson, Moshi 등)를 사용한다면 파싱 후 필드 존재 여부를 확인하는 것이 가장 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/com/flint/data/di/interceptor/NetworkErrorInterceptor.kt`
around lines 29 - 31, The current detection uses
response.peekBody(Long.MAX_VALUE).string().contains("\"errorCode\"") which risks
OOM and false positives; change NetworkErrorInterceptor to call
response.peekBody with a bounded buffer (e.g., 8KB–64KB) instead of
Long.MAX_VALUE and replace the naive contains check with a proper JSON field
existence test (use your project's JSON parser or JSONObject to parse the peeked
body and check for the "errorCode" key) when computing isBusinessError so large
bodies are not fully buffered and only a parsed JSON check determines presence
of the errorCode field.
| fun MyBookmarkedContentListResponseDto.toModel() : BookmarkedContentListModel { | ||
| return BookmarkedContentListModel( | ||
| totalCount = data.size, | ||
| contents = data.map { it.toModel() }.toImmutableList() | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: MyBookmarkedContentListResponseDto의 totalCount 필드 존재 여부 확인
rg -A 10 'data class MyBookmarkedContentListResponseDto' --type=ktRepository: imflint/Flint-Android
Length of output: 92
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) DTO 선언(정확한 타입/패키지 포함) 찾기
echo "== DTO 선언 위치 =="
rg -n "data class MyBookmarkedContentListResponseDto" -S .
# 2) DTO 내부 필드 중 totalCount 관련 존재 여부 확인
echo
echo "== DTO 내부 totalCount 관련 라인 =="
rg -n "MyBookmarkedContentListResponseDto[^{]*\{|totalCount|meta\.totalCount" -S app/src/main/java || true
# 3) ContentMapper 해당 메서드 확인
echo
echo "== ContentMapper.kt: toModel() 주변 =="
rg -n "fun MyBookmarkedContentListResponseDto\.toModel\(\)" -S app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt
sed -n '1,120p' app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt | nl -ba | sed -n '1,120p' >/tmp/ContentMapper_head.txt
# 출력 제한 위해 필요한 구간만 재출력
sed -n '15,60p' app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt | nl -ba
# 4) UserRepository에서 allContents.size를 totalCount로 쓰는지 확인
echo
echo "== UserRepository.kt: allContents.size 사용 여부 =="
rg -n "allContents\.size|totalCount" -S app/src/main/java/com/flint || trueRepository: imflint/Flint-Android
Length of output: 2068
🏁 Script executed:
#!/bin/bash
set -euo pipefail
DTO="app/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.kt"
MAPPER="app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt"
MODEL="app/src/main/java/com/flint/domain/model/content/BookmarkedContentListModel.kt"
REPO="app/src/main/java/com/flint/domain/repository/UserRepository.kt"
echo "== DTO: $(basename "$DTO") (head 60)=="
cat -n "$DTO" | sed -n '1,80p'
echo
echo "== DTO: tail 80 (필요 시)=="
wc -l "$DTO"
cat -n "$DTO" | tail -n 80
echo
echo "== MAPPER: $(basename "$MAPPER") (lines 1-80)=="
cat -n "$MAPPER" | sed -n '1,120p'
echo
echo "== MODEL: $(basename "$MODEL") (lines 1-120)=="
cat -n "$MODEL" | sed -n '1,120p'
echo
echo "== REPO: $(basename "$REPO") (around totalCount mapping)=="
# show only the relevant block around line ~82
sed -n '50,120p' "$REPO" | cat -nRepository: imflint/Flint-Android
Length of output: 11121
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) MyBookmarkedContentListResponseDto.toModel() 사용처 찾기
rg -n "MyBookmarkedContentListResponseDto\.toModel\(\)" -S app/src/main/java || true
rg -n "MyBookmarkedContentListResponseDto" -S app/src/main/java || true
# 2) contentApi.getBookmarkedContentList 반환 타입 확인(어디서 MyBookmarkedContentListResponseDto를 매핑하는지)
rg -n "getBookmarkedContentList\(cursor" -S app/src/main/java || true
rg -n "interface .*ContentApi|class .*ContentApi|ContentApi" -S app/src/main/java || true
# 3) apiService.getBookmarkedContentListByUserId(userId) 경로 확인
rg -n "getBookmarkedContentListByUserId" -S app/src/main/java || trueRepository: imflint/Flint-Android
Length of output: 2050
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) MyBookmarkedContentListResponseDto.toModel() 사용처 찾기
rg -n "MyBookmarkedContentListResponseDto\.toModel\(\)" -S app/src/main/java || true
rg -n "MyBookmarkedContentListResponseDto" -S app/src/main/java || true
# 2) contentApi.getBookmarkedContentList 반환 타입 확인(어디서 MyBookmarkedContentListResponseDto를 매핑하는지)
rg -n "getBookmarkedContentList\(cursor" -S app/src/main/java || true
rg -n "interface .*ContentApi|class .*ContentApi|ContentApi" -S app/src/main/java || true
# 3) apiService.getBookmarkedContentListByUserId(userId) 경로 확인
rg -n "getBookmarkedContentListByUserId" -S app/src/main/java || trueRepository: imflint/Flint-Android
Length of output: 2050
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# ContentApi: getBookmarkedContentList 응답 구조 확인
echo "== ContentApi.kt =="
sed -n '1,200p' app/src/main/java/com/flint/data/api/ContentApi.kt | cat -n
echo
echo "== MyBookmarkedContentListResponseDto.toModel() 호출처 =="
rg -n "MyBookmarkedContentListResponseDto\.toModel\(" -S app/src/main/java || true
echo
echo "== meta.returned 사용처 =="
rg -n "meta\.returned|returned\s*:" -S app/src/main/java || trueRepository: imflint/Flint-Android
Length of output: 1972
totalCount 산출 근거를 DTO/사용처 기준으로 정리.
MyBookmarkedContentListResponseDto(커서 페이지네이션)에는meta.totalCount같은 전체 개수 필드가 없고,meta에는returned와nextCursor만 있습니다. 따라서 현재totalCount = data.size는 “해당 응답에 포함된 아이템 수” 기준입니다.- 더 중요한 점:
UserRepository에서는userId == null경로에서 커서를 반복 누적해BookmarkedContentListModel(totalCount = allContents.size)로 전체 개수를 계산하므로, 해당MyBookmarkedContentListResponseDto.toModel()의totalCount값은 현재 코드 경로에서 실제 결과에 반영되지 않는 상태입니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt` around
lines 23 - 28, The current MyBookmarkedContentListResponseDto.toModel() sets
totalCount = data.size which only reflects items in the current cursor page;
change it to use the DTO's meta.returned value when available (e.g., totalCount
= meta?.returned ?: data.size) so the model's totalCount reflects the DTO's
intended count metric, and leave UserRepository's aggregation logic (which
builds BookmarkedContentListModel(totalCount = allContents.size)) to override as
needed; update the MyBookmarkedContentListResponseDto.toModel() implementation
accordingly.
| suspend fun toggleCollectionBookmark(collectionId: String): Result<Boolean> = | ||
| suspendRunCatching { api.toggleCollectionBookmark(collectionId).data } | ||
| .also { result -> | ||
| result.onSuccess { isBookmarked -> | ||
| _bookmarkChanges.emit(BookmarkChange.Collection(collectionId, isBookmarked)) | ||
| } | ||
| } | ||
|
|
||
| // 콘텐츠 북마크 토글 | ||
| suspend fun toggleContentBookmark(contentId: String): Result<Boolean> = | ||
| suspendRunCatching { api.toggleContentBookmark(contentId).data } | ||
| .also { result -> | ||
| result.onSuccess { isBookmarked -> | ||
| _bookmarkChanges.emit(BookmarkChange.Content(contentId, isBookmarked)) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and print the relevant repository file
FILE="app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt"
echo "=== $FILE (lines around toggle methods) ==="
if [ -f "$FILE" ]; then
nl -ba "$FILE" | sed -n '1,220p'
else
echo "File not found: $FILE"
exit 1
fi
# Find the extension/helper suspendRunCatching definition
echo
echo "=== Find suspendRunCatching definition ==="
rg -n "suspendRunCatching" -S app/src/main/java || true
# Find definitions/usages of _bookmarkChanges.emit and BookmarkChange
echo
echo "=== Find _bookmarkChanges definition and type ==="
rg -n "_bookmarkChanges" -S app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt || true
echo
echo "=== Find BookmarkChange sealed/interface ==="
rg -n "sealed class BookmarkChange|BookmarkChange\\." -S app/src/main/java || true
# Find any custom Result.onSuccess or imports (rare but check)
echo
echo "=== Check imports in BookmarkRepository.kt ==="
nl -ba "$FILE" | sed -n '1,80p'Repository: imflint/Flint-Android
Length of output: 232
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt"
echo "=== Check file exists ==="
ls -l "$FILE" || { echo "File not found: $FILE"; exit 1; }
echo
echo "=== $FILE (first 220 lines with line numbers) ==="
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'
echo
echo "=== Find suspendRunCatching definition/usages ==="
rg -n "suspendRunCatching" -S app/src/main/java
echo
echo "=== Find _bookmarkChanges definition/usages ==="
rg -n "_bookmarkChanges" -S app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt
echo
echo "=== Find BookmarkChange definitions ==="
rg -n "BookmarkChange" -S app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt
echo
echo "=== Find any onSuccess usage in BookmarkRepository.kt ==="
rg -n "\.onSuccess\s*\{" -S app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt
echo
echo "=== Show imports (top of file) ==="
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '1,80p'Repository: imflint/Flint-Android
Length of output: 9637
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for any custom Result.onSuccess extension or redefinition
rg -n "fun\s+<.*>\s*Result<.*>\.onSuccess|onSuccess\s*\(" app/src/main/java
# Show stdlib usage candidate: maybe imported extension
rg -n "onSuccess" app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt
# Inspect suspendRunCatching implementation to ensure it returns Kotlin Result and uses kotlin.Result.onSuccess
echo "=== SuspendRunCatching.kt ==="
sed -n '1,120p' app/src/main/java/com/flint/core/common/util/SuspendRunCatching.ktRepository: imflint/Flint-Android
Length of output: 47
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Search for custom Result.onSuccess extension definitions ==="
rg -n "fun\s+.*Result<.*>\.onSuccess|extension\s+Result.*onSuccess|Result<.*>\.onSuccess\s*\{" app/src/main/java || true
rg -n "fun\s+.*onSuccess\s*\(" app/src/main/java || true
echo
echo "=== Search for .onSuccess usage (limit to files to reduce output) ==="
rg -n "\.onSuccess\s*\{" app/src/main/java || true
echo
echo "=== Show SuspendRunCatching.kt ==="
FILE="app/src/main/java/com/flint/core/common/util/SuspendRunCatching.kt"
if [ -f "$FILE" ]; then
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '1,200p'
else
echo "File not found: $FILE"
fiRepository: imflint/Flint-Android
Length of output: 5451
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect WithdrawViewModel usage around line 43
FILE="app/src/main/java/com/flint/presentation/setting/withdraw/WithdrawViewModel.kt"
echo "=== $FILE (lines 1-120) ==="
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '1,140p'
echo
echo "=== Definitions related to _navigateToWithdrawComplete ==="
rg -n "_navigateToWithdrawComplete" "$FILE" || trueRepository: imflint/Flint-Android
Length of output: 2237
Result.onSuccess에서 MutableSharedFlow.emit(suspend) 호출 → 컴파일 불가(컬렉션/콘텐츠 토글 모두)
kotlin.Result.onSuccess의 람다는 non-suspend 타입이라 _bookmarkChanges.emit(...)를 호출할 수 없습니다.
🛠 제안 패치
suspend fun toggleCollectionBookmark(collectionId: String): Result<Boolean> =
- suspendRunCatching { api.toggleCollectionBookmark(collectionId).data }
- .also { result ->
- result.onSuccess { isBookmarked ->
- _bookmarkChanges.emit(BookmarkChange.Collection(collectionId, isBookmarked))
- }
- }
+ suspendRunCatching {
+ api.toggleCollectionBookmark(collectionId).data.also { isBookmarked ->
+ _bookmarkChanges.emit(BookmarkChange.Collection(collectionId, isBookmarked))
+ }
+ }
suspend fun toggleContentBookmark(contentId: String): Result<Boolean> =
- suspendRunCatching { api.toggleContentBookmark(contentId).data }
- .also { result ->
- result.onSuccess { isBookmarked ->
- _bookmarkChanges.emit(BookmarkChange.Content(contentId, isBookmarked))
- }
- }
+ suspendRunCatching {
+ api.toggleContentBookmark(contentId).data.also { isBookmarked ->
+ _bookmarkChanges.emit(BookmarkChange.Content(contentId, isBookmarked))
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| suspend fun toggleCollectionBookmark(collectionId: String): Result<Boolean> = | |
| suspendRunCatching { api.toggleCollectionBookmark(collectionId).data } | |
| .also { result -> | |
| result.onSuccess { isBookmarked -> | |
| _bookmarkChanges.emit(BookmarkChange.Collection(collectionId, isBookmarked)) | |
| } | |
| } | |
| // 콘텐츠 북마크 토글 | |
| suspend fun toggleContentBookmark(contentId: String): Result<Boolean> = | |
| suspendRunCatching { api.toggleContentBookmark(contentId).data } | |
| .also { result -> | |
| result.onSuccess { isBookmarked -> | |
| _bookmarkChanges.emit(BookmarkChange.Content(contentId, isBookmarked)) | |
| } | |
| suspend fun toggleCollectionBookmark(collectionId: String): Result<Boolean> = | |
| suspendRunCatching { | |
| api.toggleCollectionBookmark(collectionId).data.also { isBookmarked -> | |
| _bookmarkChanges.emit(BookmarkChange.Collection(collectionId, isBookmarked)) | |
| } | |
| } | |
| // 콘텐츠 북마크 토글 | |
| suspend fun toggleContentBookmark(contentId: String): Result<Boolean> = | |
| suspendRunCatching { | |
| api.toggleContentBookmark(contentId).data.also { isBookmarked -> | |
| _bookmarkChanges.emit(BookmarkChange.Content(contentId, isBookmarked)) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt` around
lines 26 - 40, The current use of Result.onSuccess in toggleCollectionBookmark
and toggleContentBookmark attempts to call the suspend function
_bookmarkChanges.emit(...) from a non-suspending lambda (Result.onSuccess),
causing a compile error; fix by replacing the onSuccess usage with a
suspend-friendly check such as val isBookmarked = result.getOrNull(); if
(isBookmarked != null) {
_bookmarkChanges.emit(BookmarkChange.Collection(collectionId, isBookmarked)) }
(and similarly for BookmarkChange.Content in toggleContentBookmark) so the emit
call runs directly in the surrounding suspend function.
| is BookmarkChange.Content -> { | ||
| val updatedContents = if (change.isBookmarked) { | ||
| data.savedContents | ||
| } else { | ||
| val filtered = data.savedContents.contents | ||
| .filter { it.id != change.id } | ||
| .toPersistentList() | ||
| data.savedContents.copy( | ||
| contents = filtered, | ||
| totalCount = filtered.size, | ||
| ) | ||
| } | ||
| data.copy(savedContents = updatedContents) | ||
| } | ||
| is BookmarkChange.Collection -> { | ||
| val updatedCollections = if (change.isBookmarked) { | ||
| data.savedCollections | ||
| } else { | ||
| val filtered = data.savedCollections.collections | ||
| .filter { it.id != change.id } | ||
| .toPersistentList() | ||
| data.savedCollections.copy(collections = filtered) | ||
| } | ||
| data.copy(savedCollections = updatedCollections) |
There was a problem hiding this comment.
타 유저 프로필의 저장 목록도 함께 지워집니다.
지금 분기는 isBookmarked == false이면 현재 보고 있는 프로필의 주인과 무관하게 항목을 목록에서 제거합니다. 그래서 다른 유저 프로필을 보고 있을 때 내가 북마크를 해제하면, 그 유저가 저장한 항목까지 사라집니다. PR 목표대로라면 내 프로필일 때만 목록에서 제거하고, 타 유저 프로필에서는 항목을 유지한 채 내 북마크 상태만 토글해야 합니다. 같은 이유로 isBookmarked == true 이벤트도 현재는 실질적으로 반영되지 않습니다.
| val filtered = data.savedContents.contents | ||
| .filter { it.id != change.id } | ||
| .toPersistentList() | ||
| data.savedContents.copy( | ||
| contents = filtered, | ||
| totalCount = filtered.size, | ||
| ) |
There was a problem hiding this comment.
totalCount를 현재 리스트 길이로 덮어쓰면 전체 개수 계약이 깨집니다.
savedContents.contents는 현재 메모리에 올라온 일부 항목일 수 있는데, 여기서 filtered.size를 넣으면 서버가 준 전체 저장 개수가 현재 화면 개수로 축소됩니다. 북마크 해제 시에는 기존 totalCount에서 1만 감소시키고, 화면 리스트 길이와 전체 개수는 분리해서 유지해야 합니다.
🛠 제안 패치
data.savedContents.copy(
contents = filtered,
- totalCount = filtered.size,
+ totalCount = (data.savedContents.totalCount - 1).coerceAtLeast(0),
)📝 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 filtered = data.savedContents.contents | |
| .filter { it.id != change.id } | |
| .toPersistentList() | |
| data.savedContents.copy( | |
| contents = filtered, | |
| totalCount = filtered.size, | |
| ) | |
| val filtered = data.savedContents.contents | |
| .filter { it.id != change.id } | |
| .toPersistentList() | |
| data.savedContents.copy( | |
| contents = filtered, | |
| totalCount = (data.savedContents.totalCount - 1).coerceAtLeast(0), | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt` around
lines 64 - 70, 현재 코드에서 savedContents.contents를 필터한 뒤 totalCount를 filtered.size로
덮어써 전체 개수 계약이 깨집니다; 대신 북마크 해제 로직(예: where change.id is removed)에서는 contents를
filtered로 갱신하되 savedContents.totalCount는 기존 totalCount에서 1만 감소시키는 방식으로 유지하세요 (즉,
data.savedContents.copy(contents = filtered, totalCount =
maxOf(data.savedContents.totalCount - 1, 0))처럼 기존 totalCount를 직접 조작),
savedContents와 화면 리스트 길이를 분리해서 다루도록 ProfileViewModel의 해당 분기(데이터 업데이트:
data.savedContents.contents, totalCount)를 수정하십시오.
📮 관련 이슈
📌 작업 내용
작업 요약
마이페이지 API를 최신 스펙에 맞게 연동했습니다.
/users/me) 적용저장한 작품 목록 기능을 개선했습니다.
북마크 상태 동기화를 추가했습니다.
취향 키워드 재계산 기능을 구현했습니다.
북마크 최소 개수 제한 처리를 서버 에러 응답 기반으로 변경했습니다.
BOOKMARK.CONTENT_MIN_LIMIT에러 수신 시 제한 안내 모달 노출😅 미구현
🫛 To. 리뷰어
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정