[Feat] CollectionDetailScreen 추가 구현#153
Conversation
- `BookmarkApi`, `CollectionApi`의 모든 엔드포인트 경로 앞에 `/` 추가하여 절대 경로로 수정
- `BookmarkApi` 및 `BookmarkRepository`에 콘텐츠 북마크 토글 API 연동 - `CollectionDetailViewModel`에서 북마크 상태 변화에 따른 UI State 업데이트 로직 추가 - `CollectionDetailScreen`에서 콘텐츠 북마크 아이콘 클릭 이벤트 연결
- `CollectionDetailModel`에 `author.id`와 `userId`를 비교하여 본인 소유 여부를 판단하는 `isMine` 프로퍼티 추가 - `CollectionDetailScreen`에서 `authorId`와 `userId`를 각각 전달하던 방식에서 `isMine` 플래그를 전달하는 방식으로 변경 - `userId` 필드의 접근 제어자를 `private`으로 변경하여 캡슐화 강화
- `Spoiler` 컴포넌트의 파라미터명을 `spoil`에서 `onSpoilClick`으로 변경 - `CollectionDetailViewModel`에 `spoil` 함수를 추가하여 특정 콘텐츠의 스포일러 상태를 해제하는 로직 구현 - `CollectionDetailScreen`에서 스포일러 버튼 클릭 시 해당 콘텐츠의 스포일러를 해제하도록 이벤트 연결
- `collectionRepository.getCollectionDetail`과 `bookmarkRepository.getCollectionBookmarkUsers`를 `async`로 감싸 병렬로 실행되도록 개선 - `await()`을 사용하여 두 요청의 결과를 기다린 후 UI 상태 업데이트하도록 변경
- `isMine`이 false일 때(본인의 컬렉션이 아닐 때)만 북마크 버튼이 보이도록 조건 변경
- 북마크 토글 시 UI를 즉시 업데이트하도록 변경 (Optimistic Update) - API 호출 실패 시 이전 상태로 복구하는 로직 추가 - 2초의 쓰로틀링(Throttling)을 적용하여 중복 호출 방지
- `toggleContentBookmark` 함수에 2초 쓰로틀링 적용 - 콘텐츠별 쓰로틀링 관리를 위해 `lastContentBookmarkToggleTime` 맵 추가 - 북마크 상태 변경 시 UI를 즉시 업데이트하고, API 실패 시 이전 상태로 롤백하는 낙관적 업데이트 로직 구현 - 기존 `lastBookmarkToggleTime` 변수명을 `lastCollectionBookmarkToggleTime`으로 변경하여 명확성 개선
📝 WalkthroughWalkthrough컬렉션 상세에 콘텐츠·컬렉션 북마크 토글(스로틀·낙관적 업데이트), 스포일러 확인, 저장자 프로필 네비게이션(userId 전파)을 도입하고 API 경로 정규화 및 관련 UI/프리뷰/모델/네비게이션 시그니처를 변경했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant Screen as CollectionDetailScreen
participant ViewModel as CollectionDetailViewModel
participant Repo as BookmarkRepository
participant API as BookmarkApi
User->>Screen: 콘텐츠 북마크 클릭 (contentId)
Screen->>ViewModel: onBookmarkIconClick(contentId)
ViewModel->>ViewModel: 로컬 상태 낙관적 변경 (isBookmarked/count)
ViewModel->>Screen: UI 즉시 갱신
ViewModel->>Repo: toggleContentBookmark(contentId)
Repo->>API: POST /api/v1/bookmarks/contents/{contentId}
API-->>Repo: BaseResponse<Boolean>
Repo-->>ViewModel: Result(success/failure)
alt 성공
ViewModel->>Screen: 상태 확정 및 ToggleContentBookmarkSuccess 발행
else 실패
ViewModel->>ViewModel: 상태 복원
ViewModel->>Screen: UI 복원
end
sequenceDiagram
participant User as 사용자
participant Screen as CollectionDetailScreen
participant Navigator as MainNavigator
User->>Screen: 저자 클릭 (authorId)
Screen->>Navigator: navigateToProfile(authorId)
Navigator->>Navigator: NavController.navigate(MainTabRoute.Profile(userId = authorId))
Navigator->>User: 프로필 화면으로 이동
sequenceDiagram
participant Init as 초기화
participant ViewModel as CollectionDetailViewModel
participant Repo as Repository
participant API as RemoteAPI
Init->>ViewModel: getCollectionDetailAndBookmarkUsers(collectionId)
ViewModel->>ViewModel: async { getCollectionDetail }
ViewModel->>ViewModel: async { getCollectionBookmarkUsers }
par 병렬 호출
ViewModel->>Repo: getCollectionDetail(collectionId)
Repo->>API: GET /api/v1/collections/{collectionId}
API-->>Repo: CollectionDetail
and
ViewModel->>Repo: getCollectionBookmarkUsers(collectionId)
Repo->>API: GET /api/v1/bookmarks/{collectionId}
API-->>Repo: Users
end
ViewModel->>ViewModel: 결과 병합 및 uiState 업데이트
ViewModel->>Init: UiState 반영
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 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
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: 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/collectiondetail/CollectionDetailScreen.kt (1)
809-860: 프리뷰 파라미터 타입 불일치로 미리보기 오류가 발생합니다.Provider가
ContentModel을 제공하지만 파라미터는ContentModelNew입니다. Provider의 제네릭 타입과 values 타입을ContentModelNew로 변경하고, ContentModel 인스턴스들을 ContentModelNew로 수정해주세요. 필드명도 맞춰야 합니다:contentId→id,posterImage→imageUrl,description→reason,ottSimpleList제거.🐛 수정 제안
-private class ContentPreviewProvider : PreviewParameterProvider<ContentModel> { - override val values: Sequence<ContentModel> = +private class ContentPreviewProvider : PreviewParameterProvider<ContentModelNew> { + override val values: Sequence<ContentModelNew> = sequenceOf( - ContentModel( - contentId = "0", - title = "드라마 제목", - year = 2000, - posterImage = "", - ottSimpleList = listOf(OttType.Netflix, OttType.Disney), - director = "가스 제닝스", - description = "달라진 온도\n-\n같은 구도에 채도를 달리해 변해버린 사랑을 시각적으로 담아낸 장면들", - isSpoiler = false, - isBookmarked = false, - ), - ContentModel( - contentId = "0", - title = "스포일러 있는 영화", - year = 2024, - posterImage = "", - ottSimpleList = listOf(OttType.Netflix), - director = "감독 이름", - description = "이 내용은 스포일러가 포함되어 있습니다.", - isSpoiler = true, - isBookmarked = false, - ), - ContentModel( - contentId = "0", - title = "저장된 영화", - year = 2023, - posterImage = "", - ottSimpleList = listOf(OttType.Watcha, OttType.Wave), - director = "다른 감독", - description = "내가 저장한 영화입니다.", - isSpoiler = false, - isBookmarked = true, - bookmarkCount = 42, - ), + ContentModelNew( + id = "0", + title = "드라마 제목", + year = 2000, + imageUrl = "", + director = "가스 제닝스", + reason = "달라진 온도\n-\n같은 구도에 채도를 달리해 변해버린 사랑을 시각적으로 담아낸 장면들", + isSpoiler = false, + isBookmarked = false, + bookmarkCount = 0, + ), + ContentModelNew( + id = "0", + title = "스포일러 있는 영화", + year = 2024, + imageUrl = "", + director = "감독 이름", + reason = "이 내용은 스포일러가 포함되어 있습니다.", + isSpoiler = true, + isBookmarked = false, + bookmarkCount = 0, + ), + ContentModelNew( + id = "0", + title = "저장된 영화", + year = 2023, + imageUrl = "", + director = "다른 감독", + reason = "내가 저장한 영화입니다.", + isSpoiler = false, + isBookmarked = true, + bookmarkCount = 42, + ), ) }
🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt`:
- Around line 84-109: The optimistic unbookmark decrement can drive
bookmarkCount below zero; when computing the new bookmarkCount in the call to
updateContentBookmarkState (around variables contentId, previousBookmarkState,
previousBookmarkCount), clamp the decrement so it never goes below 0 (e.g., use
max(0, previousBookmarkCount - 1) or conditional logic) for the optimistic
update and ensure the same protection when rolling back on failure in the
failure branch; keep updateContentIsBookmarkedOnly unchanged.
🧹 Nitpick comments (2)
app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt (2)
48-55: 쓰로틀 타이밍은 모노토닉 시계 사용을 권장합니다.벽시계 변경에 영향을 받는 시간원 대신 경과 시간 기반 시계를 쓰면 안정적입니다.
♻️ 개선 제안
+import android.os.SystemClock ... - val currentTime: Long = System.currentTimeMillis() + val currentTime: Long = SystemClock.elapsedRealtime() ... - val currentTime: Long = System.currentTimeMillis() + val currentTime: Long = SystemClock.elapsedRealtime()Also applies to: 79-82
134-151: 데이터 조회 실패 처리 TODO는 후속 작업으로 정리해 주세요.실패 시 사용자 안내/에러 상태 전환이 필요합니다. 원하시면 에러 처리 플로우 제안 드릴게요.
Also applies to: 171-173
- `bookmarkCount` 감소 시 `coerceAtLeast(0)`를 추가하여 최소값을 0으로 제한
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt`:
- Around line 53-58: Move the throttle timestamp update so it only runs after
UI/state validation succeeds: compute currentTime and check the throttle window
using lastCollectionBookmarkToggleTime and throttleDelayMs, but do not assign
lastCollectionBookmarkToggleTime = currentTime until after you have verified the
UI state (cast _uiState.value to UiState.Success and obtained
CollectionDetailUiState) and ensured content exists; update
lastCollectionBookmarkToggleTime only once validation passes and you're about to
perform the toggle. Apply the same change to the analogous block that uses the
same variables between lines 79-86.
- Around line 98-105: The code only updates isBookmarked in the
viewModelScope.launch success path (bookmarkRepository.toggleContentBookmark ->
updateContentIsBookmarkedOnly), causing bookmarkCount to drift when the server
returns a different state; change the success handling to reconcile both
bookmarked flag and count: either have bookmarkRepository.toggleContentBookmark
return the authoritative object (isBookmarked + bookmarkCount) and call a new
updater (e.g., updateContentIsBookmarkedAndCount or updateContentState) with
both values, or compute delta by reading the current item's
isBookmarked/bookmarkCount from the ViewModel state and increment/decrement the
count when calling updateContentIsBookmarkedOnly; update the success branch to
use the chosen approach so bookmarkCount stays consistent with the server.
- `toggleCollectionBookmark` 및 `toggleContentBookmark` 함수에서 UI 상태 확인을 스로틀링 체크보다 먼저 수행하도록 수정
nahy-512
left a comment
There was a problem hiding this comment.
작업 고생 너무 많으셨습니다! Throttle는 처음 보는 것 같아 신기하네요ㅎㅎ
확인된 이슈들 먼저 코멘트 남겨놓을게요.
- 스포일러 뷰에서 자물쇠 아이콘과 "스포일러가 포함된 내용이에요" 텍스트 사이 간격 확인 필요
- 작품 저장/해제 후 토스트 표시 필요
- 북마크 후 한동안 다시 클릭이 불가능한데, Throttle이 기획 의도에 잘 맞는지 한번 확인해보면 좋을 것 같습니다.
nahy-512
left a comment
There was a problem hiding this comment.
프로필까지 뚫어주셔서 감사합니다ㅎㅎ LGTM~!
…on-detail-additional-functions # Conflicts: # app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt # app/src/main/java/com/flint/presentation/main/MainNavigator.kt
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt (1)
845-898:ContentPreviewProvider와ContentPreview의 타입이 불일치합니다.
ContentPreviewProvider는ContentModel을 반환하지만,ContentPreview의 파라미터 타입은ContentModelNew입니다. 이로 인해 컴파일 오류가 발생할 수 있습니다.🐛 수정 제안
ContentPreviewProvider를ContentModelNew를 반환하도록 수정하세요:-private class ContentPreviewProvider : PreviewParameterProvider<ContentModel> { - override val values: Sequence<ContentModel> = +private class ContentPreviewProvider : PreviewParameterProvider<ContentModelNew> { + override val values: Sequence<ContentModelNew> = sequenceOf( - ContentModel( - contentId = "0", + ContentModelNew( + id = "0", title = "드라마 제목", year = 2000, - posterImage = "", - ottSimpleList = listOf(OttType.Netflix, OttType.Disney), + imageUrl = "", director = "가스 제닝스", - description = "달라진 온도\n-\n같은 구도에 채도를 달리해 변해버린 사랑을 시각적으로 담아낸 장면들", + reason = "달라진 온도\n-\n같은 구도에 채도를 달리해 변해버린 사랑을 시각적으로 담아낸 장면들", isSpoiler = false, isBookmarked = false, + bookmarkCount = 0, ), // ... 나머지 항목도 동일하게 수정 ) }
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/flint/presentation/profile/navigation/ProfileNavigation.kt`:
- Around line 12-16: The composable for MainTabRoute.Profile ignores the routed
userId; update the composable lambda to accept navBackStackEntry, extract the
route via navBackStackEntry.toRoute<MainTabRoute.Profile>() to get the userId,
and pass that userId into ProfileRoute (and into the ProfileViewModel
provider/creation) instead of always using null; ensure
navigateToProfile(authorId) now results in the ProfileRoute receiving that
authorId.
| fun NavController.navigateToProfile( | ||
| userId: String? = null, | ||
| navOptions: NavOptions? = null, | ||
| ) { | ||
| navigate(MainTabRoute.Profile(userId = userId), navOptions) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1. ProfileRoute 정의 및 userId 관련 코드 검색
rg -n --type=kt 'ProfileRoute|Profile.*userId' -A 3 -B 1
# 2. MainTabRoute.Profile 정의 및 사용 확인
rg -n --type=kt 'class.*Profile|object.*Profile' -A 5 -B 1
# 3. navigateToProfile 호출 부분 확인
rg -n --type=kt 'navigateToProfile' -B 2 -A 2
# 4. route destination 정의 확인
rg -n --type=kt '@Composable|NavGraphBuilder' -A 10 | grep -A 10 'Profile'Repository: imflint/Flint-Android
Length of output: 172
🏁 Script executed:
#!/bin/bash
# 1. ProfileRoute 및 userId 관련 코드 검색
rg -n 'ProfileRoute|MainTabRoute\.Profile' -A 3 -B 1
# 2. navigateToProfile 함수 정의 및 호출 확인
rg -n 'navigateToProfile' -B 2 -A 5
# 3. Profile route 정의 확인 (`@Composable`)
rg -n '@Composable.*Profile|route.*Profile' -A 5 -B 1
# 4. 파일 구조 확인
find . -name '*.kt' | grep -i profile | head -20Repository: imflint/Flint-Android
Length of output: 14702
🏁 Script executed:
#!/bin/bash
# 1. MainTabRoute.Profile 정의 확인
rg -n 'MainTabRoute|sealed class.*Route' -A 10 -B 1
# 2. ProfileScreen.kt의 ProfileRoute 전체 정의 확인
cat app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt | head -60
# 3. ProfileViewModel에서 userId 사용 여부 확인
rg -n 'userId' app/src/main/java/com/flint/presentation/profile/ProfileViewModel.ktRepository: imflint/Flint-Android
Length of output: 18266
ProfileRoute가 userId를 받지 않아서 라우트 매개변수가 무시됩니다
userId를 MainTabRoute.Profile에 전달하지만, composable 블록에서 ProfileRoute로 전달하지 않습니다. 또한 ProfileViewModel이 항상 userId = null로 고정되어 있어서 사용자 프로필 이동이 작동하지 않습니다. navigateToProfile(authorId) 호출도 무시되므로, composable 블록에서 navBackStackEntry.toRoute<MainTabRoute.Profile>()로 userId를 추출하여 ProfileRoute에 전달해야 합니다.
현재 코드 (ProfileNavigation.kt 25-31)
composable<MainTabRoute.Profile> {
ProfileRoute(
paddingValues = paddingValues,
navigateToCollectionList = navigateToCollectionList,
navigateToSavedContentList = navigateToSavedContentList,
navigateToCollectionDetail = navigateToCollectionDetail,
)
}🤖 Prompt for AI Agents
In
`@app/src/main/java/com/flint/presentation/profile/navigation/ProfileNavigation.kt`
around lines 12 - 16, The composable for MainTabRoute.Profile ignores the routed
userId; update the composable lambda to accept navBackStackEntry, extract the
route via navBackStackEntry.toRoute<MainTabRoute.Profile>() to get the userId,
and pass that userId into ProfileRoute (and into the ProfileViewModel
provider/creation) instead of always using null; ensure
navigateToProfile(authorId) now results in the ProfileRoute receiving that
authorId.
There was a problem hiding this comment.
PlofileRoute부터는 다른 개발자가 작업해줄거라 괜찮아.
There was a problem hiding this comment.
@giovannijunseokim, 알겠습니다! 다른 개발자분이 ProfileRoute 이후 작업을 진행하신다면 문제없습니다.
혹시 해당 작업을 추적하기 위한 이슈가 필요하시면 말씀해 주세요. 제가 생성해드릴 수 있습니다. 😊
📮 관련 이슈
📌 작업 내용
📸 스크린샷
save-collection.mp4
spoil.mp4
navigate-to-profile.mp4
after-save-collection.mp4
throttle.mp4
Summary by CodeRabbit
새로운 기능
버그 수정
개선 사항
✏️ Tip: You can customize this high-level summary in your review settings.