Skip to content

[Feature] 마이페이지 api 연결#211

Open
ckals413 wants to merge 21 commits into
developfrom
FLT-17-마이페이지-api-연결

Hidden character warning

The head ref may contain hidden characters: "FLT-17-\ub9c8\uc774\ud398\uc774\uc9c0-api-\uc5f0\uacb0"
Open

[Feature] 마이페이지 api 연결#211
ckals413 wants to merge 21 commits into
developfrom
FLT-17-마이페이지-api-연결

Conversation

@ckals413

@ckals413 ckals413 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

📮 관련 이슈

  • closed #이슈번호

📌 작업 내용

작업 요약

  • 마이페이지 API를 최신 스펙에 맞게 연동했습니다.

    • 내 프로필 조회 API(/users/me) 적용
    • 타 유저 프로필 및 저장 콘텐츠 조회 경로 수정
    • 프로필의 작품 수, 북마크 수, 키워드 재계산 가능 여부 데이터 반영
  • 저장한 작품 목록 기능을 개선했습니다.

    • 내 북마크 콘텐츠 조회를 커서 페이지네이션 방식으로 변경
    • 저장 작품 더보기 화면 및 뒤로가기 흐름 추가
    • 북마크 해제 시 목록에서 즉시 제거되도록 반영
    • 타 유저 저장 목록에서는 북마크 여부만 토글되도록 분기 처리
  • 북마크 상태 동기화를 추가했습니다.

    • 콘텐츠/컬렉션 북마크 변경 이벤트를 공유 Flow로 관리
    • 프로필 화면의 저장 콘텐츠/컬렉션 목록에 변경 상태 실시간 반영
  • 취향 키워드 재계산 기능을 구현했습니다.

    • 재계산 API 연동
    • 재계산 가능 여부에 따른 버튼 활성화 제어
    • 재계산 중 로딩 애니메이션 추가
    • 재계산 성공 후 키워드 재조회
  • 북마크 최소 개수 제한 처리를 서버 에러 응답 기반으로 변경했습니다.

    • BOOKMARK.CONTENT_MIN_LIMIT 에러 수신 시 제한 안내 모달 노출
    • 비즈니스 에러는 글로벌 네트워크 에러 emit 대상에서 제외

😅 미구현

  • 컬랙션 생성해보고 조회되는지 확인해보기

🫛 To. 리뷰어

Summary by CodeRabbit

  • 새로운 기능
    • 유저별 저장한 콘텐츠 목록 조회 지원(목록 화면에 사용자 구분 적용)
    • 프로필 키워드 재계산 기능 추가(재계산 가능 여부·진행 상태 반영)
    • 저장한 콘텐츠 목록에 커서 기반 페이지네이션 적용
  • 버그 수정
    • 네트워크 오류에서 비즈니스 오류 구분 로직 개선
    • 북마크 토글 후 카운트/상태가 즉시 동기화되도록 개선
    • 저장 목록 UI에 아이템 단위 애니메이션 반영

@ckals413 ckals413 self-assigned this Jun 11, 2026
@ckals413 ckals413 added the Feat ✨ 신규 기능을 추가하거나 기존 기능의 동작, 정책을 변경 label Jun 11, 2026
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ckals413, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 22 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77e7f06a-6648-4c76-82db-e4b285241717

📥 Commits

Reviewing files that changed from the base of the PR and between 06fc1c3 and 9b0bdf2.

📒 Files selected for processing (1)
  • app/src/main/java/com/flint/data/api/UserApi.kt
📝 Walkthrough

워크스루

PR은 Route.SavedContentList를 userId를 받는 데이터 클래스로 바꾸고, 북마크 목록 API를 커서 기반으로 전환했습니다. Bookmarked 응답/도메인 모델에 totalCount/bookmarkCount/isBookmarked가 추가되고, BookmarkRepository는 변경 이벤트를 SharedFlow로 발행합니다. Profile에 키워드 재계산 API·UI가 연결됩니다.

변경 사항

북마크 목록 및 키워드 재계산 기능

레이어 / 파일(들) 요약
저장콘텐츠 네비게이션 계약 전달
app/src/main/java/com/flint/core/navigation/Route.kt, app/src/main/java/com/flint/presentation/main/MainNavigator.kt, app/src/main/java/com/flint/presentation/main/MainNavHost.kt, app/src/main/java/com/flint/presentation/savedcontent/navigation/SavedContentNavigation.kt, app/src/main/java/com/flint/presentation/profile/...
Route.SavedContentListdata class SavedContentList(val userId: String? = null)로 변경되고, MainNavigator/MainNavHost의 네비게이션 경로와 프로필 네비게이션 그래프에서 userId와 navigateUp 콜백이 저장콘텐츠 목적지로 전달되도록 연결된다.
API 엔드포인트 및 응답 스키마 확장
app/src/main/java/com/flint/data/api/ContentApi.kt, app/src/main/java/com/flint/data/api/UserApi.kt, app/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.kt, app/src/main/java/com/flint/data/dto/user/response/UserProfileResponseDto.kt, app/src/main/java/com/flint/data/dto/base/ErrorResponseDto.kt
ContentApi getBookmarkedContentListcursor: String? = null, size: Int = 10 쿼리 파라미터를 받고 MyBookmarkedContentListResponseDto 응답을 사용하도록 변경된다. UserApi는 사용자별 북마크 경로 수정(users/{userId}/bookmarked-contents) 및 PATCH /api/v1/users/me/keywords/recalculate를 추가한다. ErrorResponseDto 및 Bookmarked 관련 DTO가 확장된다.
도메인 모델 및 매핑 함수
app/src/main/java/com/flint/domain/model/bookmark/BookmarkChange.kt, app/src/main/java/com/flint/domain/model/content/BookmarkedContentListModel.kt, app/src/main/java/com/flint/domain/model/user/UserProfileResponseModel.kt, app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt, app/src/main/java/com/flint/domain/mapper/user/ProfileMapper.kt
BookmarkChange sealed 클래스가 추가되어 콘텐츠/컬렉션 북마크 변경을 모델링한다. BookmarkedContentListModel/ItemModel에 totalCount, bookmarkCount, isBookmarked 필드가 추가되고, Profile 매퍼/모델에 keywordRecalculatable이 반영되며 매핑 함수들이 업데이트된다.
리포지토리 패턴 및 네트워크 오류 처리
app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt, app/src/main/java/com/flint/domain/repository/UserRepository.kt, app/src/main/java/com/flint/domain/repository/ContentRepository.kt, app/src/main/java/com/flint/data/di/interceptor/NetworkErrorInterceptor.kt
BookmarkRepository는 토글 성공 시 BookmarkChange 이벤트를 _bookmarkChanges.emit(...)으로 방출하고 bookmarkChanges로 외부에 노출한다. UserRepository는 ContentApi 기반 커서 누적 로직 및 recalculateKeywords() 메서드를 추가하고, getUserProfile(userId)가 명시적으로 분기된다. NetworkErrorInterceptor는 응답 바디의 errorCode 존재 시 전역 ConnectionError emit을 건너뜀으로써 비즈니스 오류를 구분한다.
프로필 키워드 재계산
app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt, app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt, app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt, app/src/main/java/com/flint/presentation/profile/uistate/ProfileUiState.kt
ProfileViewModel에 recalculateKeywords() 공개 메서드와 bookmarkChanges 관찰 로직이 추가된다. ProfileScreen은 refresh 액션을 viewModel에 연결하고 ProfileKeywordSection은 isRecalculatable/isRecalculating 파라미터를 받아 버튼 상태와 무한 회전 애니메이션을 제어한다. ProfileUiState에 isRecalculating 상태가 추가된다.
저장콘텐츠 ViewModel 및 화면 리팩터링
app/src/main/java/com/flint/presentation/profile/SavedContentViewModel.kt, app/src/main/java/com/flint/presentation/profile/SavedContentScreen.kt, app/src/main/java/com/flint/presentation/savedcontent/SavedContentListScreen.kt, app/src/main/java/com/flint/presentation/profile/uistate/SavedContentUiState.kt
SavedContentViewModel이 SavedStateHandle의 userId로 사용자별 북마크를 UserRepository를 통해 조회하고, 토글 결과는 내 프로필(userId == null)일 때 항목 제거/totalCount 감소, 타인 프로필일 때는 isBookmarked 토글만 수행한다. SavedContentScreen은 리스트 아이템별 애니메이션과 실제 bookmarkCount/isBookmarked를 사용하며, SavedContentUiState의 totalCount getter가 모델의 totalCount를 읽도록 변경된다.

수열 다이어그램

sequenceDiagram
  participant User
  participant ProfileScreen
  participant MainNavigator
  participant SavedContentViewModel
  participant UserRepository
  participant BookmarkRepository
  participant BookmarkApi
  User->>ProfileScreen: "저장작품 전체 보기" 클릭
  ProfileScreen->>MainNavigator: navigateToSavedContent(userId)
  MainNavigator->>SavedContentViewModel: Route.SavedContentList(userId)
  SavedContentViewModel->>UserRepository: getUserBookmarkedContents(userId)
  UserRepository->>BookmarkApi: 커서 기반 페이지네이션 호출
  BookmarkApi-->>UserRepository: MyBookmarkedContentListResponseDto
  UserRepository-->>SavedContentViewModel: BookmarkedContentListModel
  SavedContentViewModel->>SavedContentViewModel: 상태 갱신
  Note over SavedContentViewModel: UiState.Success(data)
Loading

예상 코드 리뷰 난이도

🎯 4 (Complex) | ⏱️ ~60분

관련 PR들

제안 라벨

🔖 API

제안 리뷰어

  • kimjw2003
  • chanmi1125

시 🐰

북마크가 살랑 춤추네, 커서 따라 흐르고
프로필의 버튼은 빙글빙글 새로워지네
유저별 목록, 하나로 모여 반짝이니
토글의 속삭임은 SharedFlow로 전해오고
토끼가 기뻐 뛰며 축하해 🐇✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive 설명은 필수 섹션들을 포함하고 있으나, 관련 이슈 번호가 '이슈번호' 플레이스홀더로만 표기되어 실제 이슈 번호가 명시되지 않았습니다. 관련 이슈 번호를 구체적으로 작성하고, 구현 체크리스트의 미구현 항목도 명확히 표시하세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 마이페이지 API 연결이라는 주요 작업을 명확하게 요약하고 있으며, 변경사항의 핵심을 잘 반영하고 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch FLT-17-마이페이지-api-연결

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: 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에서 Retrofit addConverterFactorykotlinx.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 value

savedcontent 패키지에서 profile 패키지로의 크로스 패키지 의존성

SavedContentListRouteprofile.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줄). 이 접근 방식은 다음과 같은 문제가 있습니다:

  1. 에러 응답 구조 변경 시 파싱 실패 가능
  2. JSON 파싱 예외 처리가 runCatching으로만 되어 있어 실패 원인 추적 어려움
  3. 타입 안정성 부재

다만 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.FakeListBookmarkedContentItemModel 인스턴스들이 isBookmarkedbookmarkCount 속성을 설정하지 않고 있습니다. 이제 실제 화면에서 이 속성들을 사용하므로(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

📥 Commits

Reviewing files that changed from the base of the PR and between 8d221be and 6d0d6e1.

📒 Files selected for processing (25)
  • app/src/main/java/com/flint/core/navigation/Route.kt
  • app/src/main/java/com/flint/data/api/ContentApi.kt
  • app/src/main/java/com/flint/data/api/UserApi.kt
  • app/src/main/java/com/flint/data/di/interceptor/NetworkErrorInterceptor.kt
  • app/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.kt
  • app/src/main/java/com/flint/data/dto/user/response/UserProfileResponseDto.kt
  • app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt
  • app/src/main/java/com/flint/domain/mapper/user/ProfileMapper.kt
  • app/src/main/java/com/flint/domain/model/bookmark/BookmarkChange.kt
  • app/src/main/java/com/flint/domain/model/content/BookmarkedContentListModel.kt
  • app/src/main/java/com/flint/domain/model/user/UserProfileResponseModel.kt
  • app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt
  • app/src/main/java/com/flint/domain/repository/UserRepository.kt
  • 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/profile/ProfileScreen.kt
  • app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt
  • app/src/main/java/com/flint/presentation/profile/SavedContentScreen.kt
  • app/src/main/java/com/flint/presentation/profile/SavedContentViewModel.kt
  • app/src/main/java/com/flint/presentation/profile/component/ProfileKeywordSection.kt
  • app/src/main/java/com/flint/presentation/profile/navigation/ProfileNavigation.kt
  • app/src/main/java/com/flint/presentation/profile/uistate/ProfileUiState.kt
  • app/src/main/java/com/flint/presentation/profile/uistate/SavedContentUiState.kt
  • app/src/main/java/com/flint/presentation/savedcontent/SavedContentListScreen.kt
  • app/src/main/java/com/flint/presentation/savedcontent/navigation/SavedContentNavigation.kt

Comment thread app/src/main/java/com/flint/data/di/interceptor/NetworkErrorInterceptor.kt Outdated
Comment thread app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt Outdated
Comment thread app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt Outdated
Comment thread app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt Outdated
@ckals413

ckals413 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author
  • UserProfileResponseDto의 Gson @SerializedName은 kotlinx @SerialName으로 변경했습니다.
  • SavedContentViewModel의 북마크 최소 개수 에러 판별은 ErrorResponseDto + 주입된 kotlinx Json 파싱으로 정리했습니다.
  • SavedContentScreen 프리뷰 데이터에는 bookmarkCount, isBookmarked 값을 추가했습니다.
  • savedcontent -> profile 위임 구조 코멘트는 현재 SavedContentListRoute가 기존 SavedContentRoute를 재사용하는 얇은 adapter 역할이고, 이번 PR에서 화면 위치를 옮기면 변경 범위가 커져서 유지했습니다.

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

전반적으로 설계가 깔끔하고, 북마크 상태 동기화를 SharedFlow로 처리한 방식도 좋습니다 👍 몇 가지 확인이 필요한 부분 남겼어요!

BookmarkedContentListModel(
totalCount = allContents.size,
contents = allContents.toImmutableList()
)

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.

[페이지네이션 루프] 북마크가 많은 유저의 경우 do-while 루프로 전체 페이지를 한 번에 다 불러오면 요청 횟수가 늘어나고 메모리 사용량도 커질 수 있어요. 지금은 단순하게 전부 로드하는 게 MVP에선 괜찮지만, 추후 SavedContentListScreen에서 실제 무한스크롤(커서 기반 페이지네이션)로 교체하는 걸 고려해보면 좋겠어요!

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.

count API가 분리되면서 totalCount = allContents.size 방식은 /api/v1/contents/bookmarks/count를 별도 호출하는 방식으로 수정했습니다.

BookmarkedContentListModel(
totalCount = allContents.size,
contents = allContents.toImmutableList()
)

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.

[고민해볼 점 - 페이지네이션] 북마크가 많은 유저의 경우 do-while 루프로 전체 페이지를 한 번에 다 불러오면 API 요청이 여러 번 순차적으로 발생하고, 모든 결과를 메모리에 올리게 돼요. 지금 구조에서 MVP 단순화로 이해하긴 하는데, 추후 SavedContentListScreen에서 실제 무한스크롤(커서 기반)로 개선하면 초기 로딩 속도도 빠르고 메모리도 절약될 것 같아요!

}
}
state.copy(sectionData = UiState.Success(updated))
}

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.

[버그 가능성] BookmarkChange.ContentuserId == null(내 프로필)일 때만 목록에서 제거하고, 타 유저 프로필에서는 isBookmarked 토글만 하도록 분기하고 있는데요, BookmarkChange.Collection 쪽은 이 분기가 없어서 타 유저 프로필을 보다가 컬렉션 북마크를 해제하면 상대방의 저장 컬렉션 목록에서 해당 항목이 사라지는 문제가 발생할 수 있어요.
Content와 동일하게 userId 기준으로 분기해주는 게 좋을 것 같아요!

is BookmarkChange.Collection -> {
    val updatedCollections = if (userId == null) {
        // 내 프로필: 북마크 취소 시 목록에서 제거
        if (change.isBookmarked) data.savedCollections
        else { ... filter ... }
    } else {
        // 타 유저 프로필: isBookmarked 토글만
        data.savedCollections.copy(
            collections = data.savedCollections.collections
                .map { if (it.id == change.id) it.copy(isBookmarked = change.isBookmarked) else it }
                .toPersistentList()
        )
    }
    ...
}

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.

BookmarkChange.Collection에도 userId 기준 분기를 추가했습니다. 내 프로필에선 기존처럼 목록에서 제거하고, 타 유저 프로필에선 isBookmarked 토글만 하도록 수정했습니다.

)
}

// /api/v1/contents/bookmarks (내 프로필)

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.

[사용되지 않는 코드]toModel()UserRepository.getUserBookmarkedContents 내부에서 page.data.map { it.toModel() }로 항목 단위 매핑만 쓰고, MyBookmarkedContentListResponseDto.toModel() 자체는 실제로 호출되지 않아요. 그리고 totalCount = meta.returned는 단일 페이지의 반환 수라서, 전체 목록 수와도 달라서 혼란을 줄 수 있어요. 혹시 추후 단일 페이지 조회로 바꿀 계획이 있으시면 남겨두셔도 좋고, 아니라면 제거하는 게 깔끔할 것 같아요!

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.

MyBookmarkedContentListResponseDto.toModel() 제거했습니다. 단일 페이지 조회로 전환할 계획은 현재 없어서 같이 정리했습니다!

.onFailure { throwable ->
if (throwable.isContentMinLimitError()) {
_uiState.update { it.copy(showBookmarkRestrictionModal = true) }
} else {

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.

[아키텍처] ViewModel에서 Json을 직접 주입받아 에러 바디를 파싱하는 건 데이터 레이어의 관심사를 presentation 레이어로 올린 케이스예요. BookmarkRepository.toggleContentBookmark에서 HTTP 응답을 파싱해 도메인 예외(ContentMinLimitException 같은 sealed class)로 변환하면, ViewModel은 에러 타입만 보고 분기할 수 있어서 레이어 경계가 명확해질 것 같아요. 지금 구조도 동작은 하지만, 같은 패턴이 다른 ViewModel에서도 필요해질 때 중복되기 쉬워서 한번 고민해보시면 좋겠어요!

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.

BookmarkException을 domain 레이어에 sealed class로 추가하고, BookmarkRepository.toggleContentBookmark에서 HTTP 에러 파싱 후 도메인 예외로 변환하도록 수정했습니다. SavedContentViewModel에서는 Json 주입을 제거하고 throwable is BookmarkException.ContentMinLimitExceeded로 단순화했습니다.

@kimjw2003

Copy link
Copy Markdown
Contributor

[UserApi.kt - nit] recalculateKeywords 추가 부분(83-85줄)이 2칸 들여쓰기로 되어있어요. 파일 내 나머지 함수들은 4칸이라 맞춰주시면 좋겠어요! Response<Unit> 뒤에 trailing whitespace도 같이 정리해주세요 😊

@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

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/presentation/profile/ProfileViewModel.kt (1)

152-159: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

타 유저 프로필 컨텍스트에서 키워드 재계산을 차단해야 합니다.

Line [152]~Line [159]는 userId != null이어도 실행 가능하고, 성공 시 getUserKeywords(userId = null)로 내 키워드를 현재 화면에 반영할 수 있습니다. ViewModel에서 명시적으로 가드해 컨텍스트 오염을 막아주세요.

🛠 제안 패치
 fun recalculateKeywords() = viewModelScope.launch {
+    if (userId != null) return@launch
+
     _uiState.update { it.copy(isRecalculating = true) }
     userRepository.recalculateKeywords()
         .onSuccess {
             // 버튼 즉시 비활성화 후 키워드 재조회
             _uiState.update { it.copy(profile = it.profile.copy(keywordRecalculatable = false)) }
-            userRepository.getUserKeywords(userId = null)
+            userRepository.getUserKeywords(userId = userId)
🤖 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 152 - 159, The recalculateKeywords() function in ProfileViewModel should
not execute when viewing another user's profile (when userId is not null). Add a
guard clause at the beginning of the recalculateKeywords() function to check if
userId is not null and return early if true, preventing the keyword
recalculation logic and subsequent getUserKeywords(userId = null) call from
executing in other user's profile context. This prevents context pollution where
the current user's keywords could be inadvertently updated while viewing another
user's profile.
🤖 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/domain/model/bookmark/BookmarkException.kt`:
- Around line 3-5: The ContentMinLimitExceeded exception is declared as a
singleton object, which causes the exception instance to be reused across
multiple throws, leading to shared stacktraces and internal state that degrades
debugging reliability. Change ContentMinLimitExceeded from an object declaration
to a class declaration so that a new exception instance is created each time it
is thrown. This ensures each thrown exception has its own independent stacktrace
and state for proper error tracking and debugging.

---

Outside diff comments:
In `@app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt`:
- Around line 152-159: The recalculateKeywords() function in ProfileViewModel
should not execute when viewing another user's profile (when userId is not
null). Add a guard clause at the beginning of the recalculateKeywords() function
to check if userId is not null and return early if true, preventing the keyword
recalculation logic and subsequent getUserKeywords(userId = null) call from
executing in other user's profile context. This prevents context pollution where
the current user's keywords could be inadvertently updated while viewing another
user's profile.
🪄 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: 914b0829-4e61-4d25-acd1-9018dd6ff2a9

📥 Commits

Reviewing files that changed from the base of the PR and between d2e2e29 and 06fc1c3.

📒 Files selected for processing (13)
  • app/src/main/java/com/flint/core/navigation/Route.kt
  • app/src/main/java/com/flint/data/api/ContentApi.kt
  • app/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.kt
  • app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt
  • app/src/main/java/com/flint/domain/model/bookmark/BookmarkException.kt
  • app/src/main/java/com/flint/domain/repository/BookmarkRepository.kt
  • app/src/main/java/com/flint/domain/repository/ContentRepository.kt
  • app/src/main/java/com/flint/domain/repository/UserRepository.kt
  • 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/profile/ProfileScreen.kt
  • app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt
  • app/src/main/java/com/flint/presentation/profile/SavedContentViewModel.kt
💤 Files with no reviewable changes (3)
  • app/src/main/java/com/flint/domain/repository/ContentRepository.kt
  • app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt
  • app/src/main/java/com/flint/domain/mapper/content/ContentMapper.kt
🚧 Files skipped from review as they are similar to previous changes (6)
  • app/src/main/java/com/flint/core/navigation/Route.kt
  • app/src/main/java/com/flint/presentation/main/MainNavigator.kt
  • app/src/main/java/com/flint/data/api/ContentApi.kt
  • app/src/main/java/com/flint/presentation/main/MainNavHost.kt
  • app/src/main/java/com/flint/domain/repository/UserRepository.kt
  • app/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.kt

Comment on lines +3 to +5
sealed class BookmarkException : Exception() {
/** 최소 저장 작품 수 제한으로 콘텐츠 북마크 해제 불가 */
object ContentMinLimitExceeded : BookmarkException()

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 | ⚡ Quick win

Throwableobject로 선언하면 예외 인스턴스가 재사용됩니다.

Line [5]의 singleton 예외는 반복 throw 시 stacktrace/내부 상태가 공유되어 디버깅 신뢰도가 떨어집니다. 예외는 매번 새 인스턴스로 생성되는 class로 바꾸는 편이 안전합니다.

🛠 제안 패치
 sealed class BookmarkException : Exception() {
     /** 최소 저장 작품 수 제한으로 콘텐츠 북마크 해제 불가 */
-    object ContentMinLimitExceeded : BookmarkException()
+    class ContentMinLimitExceeded : BookmarkException()
 }
// throw 지점 예시 (BookmarkRepository)
throw BookmarkException.ContentMinLimitExceeded()
🤖 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/model/bookmark/BookmarkException.kt`
around lines 3 - 5, The ContentMinLimitExceeded exception is declared as a
singleton object, which causes the exception instance to be reused across
multiple throws, leading to shared stacktraces and internal state that degrades
debugging reliability. Change ContentMinLimitExceeded from an object declaration
to a class declaration so that a new exception instance is created each time it
is thrown. This ensures each thrown exception has its own independent stacktrace
and state for proper error tracking and debugging.

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants