Skip to content

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

Merged
kimjw2003 merged 22 commits into
developfrom
FLT-17-마이페이지-api-연결
Jun 18, 2026

Hidden character warning

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

[Feature] 마이페이지 api 연결#211
kimjw2003 merged 22 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf57c563-1692-462c-9b26-c4db9a8fb3ca

📥 Commits

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

📒 Files selected for processing (3)
  • app/src/main/java/com/flint/data/api/UserApi.kt
  • app/src/main/java/com/flint/presentation/main/MainNavHost.kt
  • app/src/main/java/com/flint/presentation/main/MainNavigator.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • 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/data/api/UserApi.kt

📝 Walkthrough

워크스루

이 PR은 프로필과 저장콘텐츠 화면 간 사용자 맥락을 userId 매개변수로 전달하도록 라우트를 변경하고, 북마크 목록 조회 API를 커서 기반 페이지네이션으로 전환합니다. 북마크 변경을 SharedFlow로 발행하여 화면 상태를 동기화하고, 키워드 재계산 기능을 API 및 UI에 연결하며, 저장콘텐츠 ViewModel을 완전히 리팩터링하여 userId 기반 로드와 토글을 수행합니다.

변경 사항

프로필·저장콘텐츠 통합 플로우 개편

레이어 / 파일(들) 요약
저장콘텐츠 라우트 및 네비게이션 계약
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/ProfileScreen.kt, app/src/main/java/com/flint/presentation/profile/navigation/ProfileNavigation.kt
Route.SavedContentListdata class SavedContentList(val userId: String? = null)로 변경되어 사용자별 저장콘텐츠 조회를 지원한다. MainNavigator와 MainNavHost는 userId를 라우트에 전달하고, 프로필 네비게이션 그래프에서 userId 인자를 받는 콜백으로 네비게이션을 연결한다. SavedContentListRoute는 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의 getBookmarkedContentListcursorsize 쿼리 파라미터를 받아 커서 기반 페이지네이션을 지원하고 MyBookmarkedContentListResponseDto 응답을 사용한다. UserApi에 PATCH /api/v1/users/me/keywords/recalculate 엔드포인트가 추가된다. BookmarkedContentResponseDtobookmarkCount와 기본값 trueisBookmarked를 포함하고, UserProfileResponseDtokeywordRecalculatable 필드를 추가하며 직렬화 어노테이션이 @SerialName으로 변경된다. ErrorResponseDto가 새로 정의되어 비즈니스 에러 구분을 지원한다.
도메인 모델: 북마크 변경 및 프로필 확장
app/src/main/java/com/flint/domain/model/bookmark/BookmarkChange.kt, app/src/main/java/com/flint/domain/model/bookmark/BookmarkException.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 클래스와 Content/Collection 구체 타입이 추가되어 북마크 상태 변경을 모델링한다. BookmarkExceptionContentMinLimitExceeded 싱글톤을 정의한다. BookmarkedContentListModelBookmarkedContentItemModeltotalCount, bookmarkCount, isBookmarked 필드를 추가하고, UserProfileResponseModelprofileImageUrlkeywordRecalculatable을 추가한다. 매퍼 함수들이 새 필드를 반영하도록 확장된다.
저장소 실행 경로 및 네트워크 오류 처리
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@Singleton으로 변경되고 SharedFlow<BookmarkChange>를 통해 북마크 변경 이벤트를 발행하며, 토글 성공 시 BookmarkChange를 emit하고 ContentMinLimitExceeded 예외를 처리한다. UserRepositoryContentApi를 주입받아 userId==null일 때 커서 누적 로직으로 전체 북마크를 조회하고, getUserProfile은 userId 여부에 따라 분기 호출하며, recalculateKeywords() 메서드를 추가한다. NetworkErrorInterceptor는 응답 바디의 errorCode 존재 여부로 비즈니스 에러를 판정하여 전역 ConnectionError emit을 건너뛴다. ContentRepositorygetBookmarkedContentList 메서드가 제거된다.
프로필 키워드 재계산: ViewModel·UI·상태
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
ProfileViewModelBookmarkRepository를 주입받아 bookmarkChanges를 수집하고 savedContents/savedCollections을 갱신하며, 공개 메서드 recalculateKeywords()를 추가하여 키워드 재계산과 프로필 갱신을 수행한다. ProfileScreen은 refresh 액션을 viewModel.recalculateKeywords에 연결하고 SavedContentsSection의 "전체 보기"를 활성화한다. ProfileKeywordSectionisRecalculatableisRecalculating 파라미터로 버튼 상태와 무한 회전 애니메이션을 제어한다. ProfileUiStateisRecalculating 필드가 추가된다.
저장콘텐츠 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
SavedContentViewModelSavedStateHandle의 userId로 userRepository.getUserBookmarkedContents(userId)를 호출하여 사용자별 북마크를 조회한다. toggleBookmarkbookmarkRepository.toggleContentBookmark() 응답으로 해당 항목의 isBookmarked만 갱신하고, ContentMinLimitExceeded 예외 시 제한 모달을 표시한다. SavedContentUiStatetotalCount getter가 모델의 totalCount 필드를 읽도록 변경되고, SavedContentScreen은 리스트 아이템별 애니메이션과 실제 bookmarkCount/isBookmarked 값을 사용하며, 프리뷰는 totalCount와 contents를 함께 전달한다.

수열 다이어그램

sequenceDiagram
  participant User
  participant ProfileScreen
  participant MainNavigator
  participant SavedContentViewModel
  participant UserRepository
  participant BookmarkRepository
  participant ContentApi
  User->>ProfileScreen: "저장작품 전체 보기" 클릭
  ProfileScreen->>MainNavigator: navigateToSavedContent(userId)
  MainNavigator->>SavedContentViewModel: Route.SavedContentList(userId)
  SavedContentViewModel->>UserRepository: getUserBookmarkedContents(userId)
  alt userId == null (내 프로필)
    UserRepository->>ContentApi: 커서 페이지네이션 반복 호출
    ContentApi-->>UserRepository: MyBookmarkedContentListResponseDto
  else userId != null (타인 프로필)
    UserRepository->>UserRepository: apiService 경로로 조회
  end
  UserRepository-->>SavedContentViewModel: BookmarkedContentListModel
  SavedContentViewModel->>SavedContentViewModel: UiState.Success(data) 갱신
  Note over User,SavedContentViewModel: 사용자가 북마크 토글
  User->>SavedContentViewModel: 북마크 클릭
  SavedContentViewModel->>BookmarkRepository: toggleContentBookmark(contentId)
  BookmarkRepository-->>SavedContentViewModel: isBookmarked 결과
  BookmarkRepository->>BookmarkRepository: bookmarkChanges.emit(BookmarkChange.Content(...))
Loading

예상 코드 리뷰 난이도

🎯 4 (Complex) | ⏱️ ~70분

관련 PR들

  • imflint/Flint-Android#204: SavedContent 화면 및 뷰모델 리팩터링의 선행 변경사항과 강하게 연관됨.
  • imflint/Flint-Android#203: ProfileKeywordSection 및 프로필 화면 콜백·파라미터 변경이 중복되어 코드 수준 연동.
  • imflint/Flint-Android#212: BookmarkedContentListResponseDto 및 북마크 API 응답 구조 변경과 직접 충돌/연동 가능.

제안 라벨

🔖 API

제안 리뷰어

  • kimjw2003
  • chanmi1125

시 🐰

북마크가 유저마다 살랑 춤을 추네,
커서 따라 흐르는 페이지네이션
키워드는 빙글빙글 다시 계산되고,
SharedFlow로 토글의 속삭임이 전해져,
토끼가 뛰며 축하해 이 아름다운 통합을 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 마이페이지 API 연결이라는 주요 변경사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 작업 요약, 개선사항, 구현된 기능들을 상세히 기술하고 있으나 관련 이슈 번호가 누락되었습니다.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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

@kimjw2003 kimjw2003 merged commit 7ad37b2 into develop Jun 18, 2026
2 checks passed
@kimjw2003 kimjw2003 deleted the FLT-17-마이페이지-api-연결 branch June 18, 2026 07:27
@coderabbitai coderabbitai Bot mentioned this pull request Jun 18, 2026
7 tasks
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