Skip to content

[Feat] CollectionList 화면 유형 구분 및 컬렉션 조회 API 연동#149

Merged
nahy-512 merged 14 commits intodevelopfrom
feat/#148-collection-list-api
Jan 21, 2026
Merged

[Feat] CollectionList 화면 유형 구분 및 컬렉션 조회 API 연동#149
nahy-512 merged 14 commits intodevelopfrom
feat/#148-collection-list-api

Conversation

@nahy-512
Copy link
Copy Markdown
Contributor

@nahy-512 nahy-512 commented Jan 21, 2026

📮 관련 이슈

📌 작업 내용

  • CollectionListScreenCollectionListRouteType을 추가하고, 화면 이동 시에 타입을 통해 분기처리 해줬어요.
  • RECENT/CREATED/SAVED에 따른 컬렉션 조회 API를 연동했어요.

📸 스크린샷

컬렉션 목록 조회 북마크 처리
Screen_recording_20260121_120206.mp4
Screen_recording_20260121_185731.mp4

😅 미구현

  • 컬렉션 북마크 API 연동
  • 내 프로필/타인 프로필 진입 구분
  • 디샘들께 물어보고 필요하다면 ProfileScreen 스크롤 위치 개선하기 -> 지금대로도 괜찮다고 함.

🫛 To. 리뷰어

  • [Feat] Profile 컬렉션, 작품 목록 API 연동 #146 작업을 땡겨와서, 해당 PR 머지 후 draft 풀겠습니다
  • Routecore에 위치해버려서, domain에 위치한 type 내에 CollectionListRouteType를 위치시키니까 의존성이 굉장히 이상해진 것 같네요..Route 인터페이스라면 core에 위치시키는 게 맞지만, Home, CollectionList 등의 구현체들은 presentation 레이어에 위치하는 게 맞다고 생각해요. 관련해서 다른 분들은 어떻게 생각하시는지 궁금합니다.

Summary by CodeRabbit

  • 새로운 기능

    • 컬렉션 북마크 토글 및 즉시 반영되는 피드백(토스트)
    • 컬렉션 목록 타입 필터 추가 — 전체 / 저장 / 최근 선택 가능
  • 개선 사항

    • 컬렉션 화면 상태관리(UiState)로 로딩/오류/성공 처리 개선
    • 컬렉션 목록 내 북마크 버튼과 카운트 반영, 그리드 항목 상호작용 추가
    • 홈/프로필/상세 등 여러 화면에서 타입 기반 컬렉션 목록 탐색 흐름 강화

✏️ Tip: You can customize this high-level summary in your review settings.

@nahy-512 nahy-512 self-assigned this Jan 21, 2026
@nahy-512 nahy-512 requested a review from a team as a code owner January 21, 2026 03:11
@nahy-512 nahy-512 added 🔖 API feat - API 연동 Feat ✨ 신규 기능을 추가하거나 기존 기능의 동작, 정책을 변경 labels Jan 21, 2026
@nahy-512 nahy-512 linked an issue Jan 21, 2026 that may be closed by this pull request
4 tasks
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 21, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

컬렉션 목록 라우트에 타입 파라미터(CollectionListRouteType)를 도입하고, 컬렉션 목록 UI와 뷰모델에 북마크 토글 및 사이드이펙트를 추가하며 관련 네비게이션/도메인 모델 시그니처를 갱신했습니다. 레거시 모델과 샘플 데이터 일부가 제거되었습니다.

Changes

Cohort / File(s) 변경 사항
라우트 & 라우트 타입
\app/src/main/java/com/flint/core/navigation/Route.kt`, `app/src/main/java/com/flint/core/navigation/model/CollectionListRouteType.kt``
Route.CollectionList를 싱글턴에서 data class CollectionList(val routeType: CollectionListRouteType)로 변경하고 CollectionListRouteType(CREATED, SAVED, RECENT) 추가
네비게이션 통합(전파)
\app/src/main/java/com/flint/presentation/collectionlist/navigation/...`, `app/src/main/java/com/flint/presentation/home/navigation/...`, `app/src/main/java/com/flint/presentation/profile/navigation/...`, `app/src/main/java/com/flint/presentation/collectiondetail/navigation/...`, `app/src/main/java/com/flint/presentation/main/... ``
여러 NavGraph/네비게이션 함수들의 navigateToCollectionList 시그니처를 () -> Unit에서 (CollectionListRouteType) -> Unit으로 변경 및 호출부 전파, NavController 확장 호출에 routeType 전달
컬렉션 목록 UI 및 컴포넌트
\app/src/main/java/com/flint/presentation/collectionlist/CollectionListScreen.kt`, `app/src/main/java/com/flint/presentation/collectionlist/component/CollectionFileItem.kt``
UiState 기반 렌더링으로 전환( UiState<CollectionListModel> ), CollectionDetailModelCollectionListModel/CollectionItemModel로 타입 변경, onBookmarkClick 콜백 추가, 토스트/사이드이펙트 처리 및 키/아이템 필드 갱신
뷰모델(비즈니스 로직)
\app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt``
BookmarkRepository 주입 추가, _sideEffect(MutableSharedFlow) 및 sideEffect(SharedFlow) 노출, toggleCollectionBookmark(collectionId) 구현으로 북마크 토글·uiState 업데이트·사이드이펙트 방출
상태·사이드이펙트 타입
\app/src/main/java/com/flint/presentation/collectionlist/uistate/CollectionListUiState.kt`, `app/src/main/java/com/flint/presentation/collectionlist/sideeffect/CollectionListSideEffect.kt``
CollectionListUiState 추가(collectionList: UiState<CollectionListModel>), CollectionListSideEffect 계층 및 ToggleCollectionBookmarkSuccess/Failure 추가
도메인 모델 정리
\app/src/main/java/com/flint/domain/model/collection/CollectionDetailModel.kt`, `app/src/main/java/com/flint/domain/type/SampleType.kt``
레거시 CollectionDetailModel(및 Fake 데이터) 삭제 및 SampleType enum 제거

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as CollectionListScreen
    participant VM as CollectionListViewModel
    participant Repo as BookmarkRepository
    participant Toast as UIToast

    User->>UI: onBookmarkClick(collectionId)
    UI->>VM: toggleCollectionBookmark(collectionId)
    VM->>Repo: toggleCollectionBookmark(collectionId)
    alt 성공
        Repo-->>VM: success(isBookmarked, delta)
        VM->>VM: update uiState (isBookmarked, bookmarkCount)
        VM-->>UI: emit ToggleCollectionBookmarkSuccess(isBookmarked)
        UI->>Toast: show success toast
    else 실패
        Repo-->>VM: error
        VM-->>UI: emit ToggleCollectionBookmarkFailure
        UI->>Toast: show error toast
    end
Loading
sequenceDiagram
    participant ScreenA as Profile/Home/Detail
    participant MainNav as MainNavigator
    participant Nav as NavController
    participant Route as Route(CollectionList)

    ScreenA->>MainNav: navigateToCollectionList(routeType)
    MainNav->>Nav: navigateToCollectionList(routeType = routeType)
    Nav->>Route: construct CollectionList(routeType = routeType)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • giovannijunseokim
  • kimjw2003

Poem

🐰 라우트에 타입을 더했네, 폴짝!
북마크 탭 한 번에 반짝반짝 ✨
화면들 손잡고 길을 따라,
레거시는 안녕, 새 모델로 척척.
깡총깡총 축하합니다! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 범위를 벗어난 변경사항들이 관찰됩니다. CollectionDetailModel 삭제, SampleType 삭제 등은 #148에서 요구하지 않은 변경입니다. CollectionDetailModel 제거, SampleType 제거 등이 #148의 필수 요구사항과 무관한 변경이라면, 별도 PR로 분리하거나 해당 변경의 필요성을 명확히 설명하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목 '[Feat] CollectionList 화면 유형 구분 및 컬렉션 조회 API 연동'은 PR의 주요 변경 사항을 명확하게 설명하고 있습니다.
Description check ✅ Passed PR 설명은 관련 이슈, 작업 내용, 스크린샷, 미구현 항목, 리뷰 사항을 포함하여 템플릿을 잘 따르고 있습니다.
Linked Issues check ✅ Passed PR의 변경 사항들이 #148의 요구사항(화면 유형 분기 처리, RECENT/CREATED/SAVED API 연동, 북마크 기능)을 모두 충족합니다.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@nahy-512 nahy-512 marked this pull request as draft January 21, 2026 03:12
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt`:
- Around line 28-29: _uiState is a MutableStateFlow but uiState is exposed as
MutableStateFlow, allowing external mutation; change the public property uiState
to a read-only StateFlow by declaring uiState: StateFlow<CollectionListUiState>
= _uiState (keep the private _uiState as MutableStateFlow), and add the
necessary import for kotlinx.coroutines.flow.StateFlow so callers can only
collect the state and not modify it.

In
`@app/src/main/java/com/flint/presentation/collectionlist/component/CollectionFileItem.kt`:
- Around line 53-54: The poster1Url assignment directly accesses imageList[0],
which can throw IndexOutOfBoundsException when imageList is empty; change the
assignment in the component (poster1Url) to safely access the list (e.g., use
imageList.getOrNull(0) ?: "" or imageList.firstOrNull() ?: "") and keep the
existing safe check for poster2Url (imageList.size >= 2) or similarly use
getOrNull(1) ?: "" to avoid any out-of-bounds access.
🧹 Nitpick comments (7)
app/src/main/java/com/flint/domain/model/user/UserProfileResponseModel.kt (1)

10-21: Companion object의 초기값을 null로 통일하는 것을 권장합니다.

profileImageUrl 타입이 String?로 변경되었지만, EmptyFake 객체에서는 여전히 빈 문자열 ""을 사용하고 있습니다. 이로 인해 호출부에서 null 체크와 빈 문자열 체크를 모두 해야 하는 상황이 발생할 수 있습니다.

♻️ 권장 수정안
     companion object {
         val Empty = UserProfileResponseModel(
             id = "",
             isFliner = false,
             nickname = "",
-            profileImageUrl = ""
+            profileImageUrl = null
         )
         val Fake = UserProfileResponseModel(
             id = "123",
             isFliner = true,
             nickname = "닉네임",
-            profileImageUrl = ""
+            profileImageUrl = null
         )
     }
app/src/main/java/com/flint/presentation/collectionlist/uistate/CollectionListUiState.kt (1)

6-8: @Immutable 어노테이션 추가 고려

Compose의 recomposition 최적화를 위해 @Immutable 어노테이션 추가를 고려해보세요. ProfileUiState와 일관성도 유지됩니다.

💡 제안하는 수정
+import androidx.compose.runtime.Immutable
+
+@Immutable
 data class CollectionListUiState(
     val collectionList: UiState<CollectionListModel> = UiState.Loading
 )
app/src/main/java/com/flint/data/dto/user/response/BookmarkedCollectionListResponseDto.kt (1)

30-33: 프로퍼티 명칭 정합성 개선을 고려해주세요.

profileUrlprofileImageUrl로 매핑되어 있어 의미가 어긋납니다. 도메인/매퍼 영향 범위를 고려해 profileImageUrl로 이름을 맞추는 리팩터를 검토해 주세요.

app/src/main/java/com/flint/presentation/profile/ProfileViewModel.kt (1)

46-68: 섹션 조회 실패 시 사용자 피드백 경로를 고려해 주세요.

현재 실패 시 상태 갱신 없이 종료되므로, 필요하다면 에러 상태나 안내 메시지 처리를 추가하면 UX가 안정적입니다.

app/src/main/java/com/flint/domain/mapper/collection/CollectionMapper.kt (1)

57-96: 컬렉션 아이템 매핑 중복을 줄일 여지가 있습니다.

Created/Bookmarked/Recent 매핑이 동일 필드를 반복하므로 공통 헬퍼로 묶으면 필드 변경 시 유지보수가 쉬워집니다.

app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt (1)

22-23: Repository를 private으로 변경하세요

userRepositorycollectionRepositoryval로 선언되어 public 프로퍼티로 노출됩니다. ViewModel의 내부 의존성은 외부에서 접근할 필요가 없으므로 private으로 캡슐화하는 것이 좋습니다.

♻️ 권장 수정사항
 `@HiltViewModel`
 class CollectionListViewModel `@Inject` constructor(
     savedStateHandle: SavedStateHandle,
-    val userRepository: UserRepository,
-    val collectionRepository: CollectionRepository,
+    private val userRepository: UserRepository,
+    private val collectionRepository: CollectionRepository,
 ) : ViewModel() {
app/src/main/java/com/flint/presentation/collectionlist/CollectionListScreen.kt (1)

73-131: Failure 상태에 대한 사용자 피드백 고려

else 브랜치(Line 130)가 비어있어 UiState.Failure 상태에서 사용자에게 아무런 피드백이 없습니다. 에러 메시지나 재시도 버튼을 표시하는 것이 UX 관점에서 좋습니다.

현재 스프린트 범위가 아니라면 TODO 주석을 남겨두는 것을 권장합니다.

♻️ 에러 상태 처리 예시
-            else -> {}
+            is UiState.Failure -> {
+                // TODO: 에러 상태 UI 구현 (에러 메시지, 재시도 버튼 등)
+            }
+
+            else -> {}

@nahy-512 nahy-512 force-pushed the feat/#148-collection-list-api branch from 6fc0653 to 0f2572d Compare January 21, 2026 04:39
@nahy-512 nahy-512 marked this pull request as ready for review January 21, 2026 07:50
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@app/src/main/java/com/flint/presentation/collectionlist/CollectionListScreen.kt`:
- Around line 127-184: The when-block handling collectionList only covers
UiState.Loading and UiState.Success, leaving error/empty states rendering blank;
add explicit branches for UiState.Empty and UiState.Error inside the same
when(collectionList) to render a minimal Empty/Error UI (message + optional
icon) and a retry action (wire to an existing reload callback or add a new
onRetry lambda parameter to the screen if none exists), e.g., implement case
UiState.Empty { Centered text "No collections" + optional create button } and
case is UiState.Error { Centered text with error message and a Button that calls
onRetry() }, keeping the rest of the layout (LazyVerticalGrid,
CollectionFileItem, onCollectionItemClick, onBookmarkClick) unchanged.

In
`@app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt`:
- Around line 71-90: When toggling bookmark state in CollectionListViewModel
(handling collectionList -> updatedCollections), ensure that on the SAVED screen
you remove the item when it is being un-bookmarked instead of leaving a toggled
item: detect the SAVED context (your existing screen/state flag) and for
collection.id == collectionId, if isBookmarked == false and current view is
SAVED exclude that collection from the resulting list; for all updates clamp
bookmarkCount to a non-negative value (e.g., bookmarkCount = max(0,
collection.bookmarkCount + if (isBookmarked) 1 else -1)). Update the code that
builds updatedCollections and the UiState.Success(CollectionListModel(...))
accordingly.
🧹 Nitpick comments (1)
app/src/main/java/com/flint/core/navigation/model/CollectionListRouteType.kt (1)

3-8: 라우팅 enum에 UI 문자열을 직접 넣지 않는 편이 좋아요.

core/navigation 모델에 한국어 문자열이 들어가면 i18n 및 레이어 분리가 깨지기 쉬워요. presentation에서 routeType → stringResource 매핑으로 분리하거나, 최소한 @StringRes로 리소스 ID만 들고 있게 정리하는 방향을 추천합니다. Based on learnings, core 계층의 UI 의존을 최소화하세요.

Comment on lines +127 to 184
when (collectionList) {
is UiState.Loading -> {
FlintLoadingIndicator()
}

Text(
text = "총 ${collections.size}개",
color = FlintTheme.colors.gray100,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
is UiState.Success -> {
with(collectionList.data) {
LazyVerticalGrid(
contentPadding = PaddingValues(10.dp),
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(horizontal = 10.dp),
) {
item(
span = { GridItemSpan(maxLineSpan) }
) {
Spacer(Modifier.height(12.dp))

Spacer(Modifier.height(24.dp))
Text(
text = "총 ${collections.size}개",
color = FlintTheme.colors.gray100,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 6.dp)
.padding(bottom = 14.dp),
)
}

LazyVerticalGrid(
contentPadding = PaddingValues(10.dp),
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier =
Modifier
.padding(horizontal = 10.dp),
) {
items(
items = collections,
key = { it.collectionId },
) { collection ->
Box(modifier = Modifier.fillMaxSize(1f).align(Alignment.CenterHorizontally)) {
CollectionFileItem(
collection = collection,
modifier =
Modifier
.align(Alignment.Center)
.noRippleClickable(
onClick = { onCollectionItemClick(collection.collectionId) }
items(
items = collections,
key = { it.id },
) { collection ->
Box(
modifier =
Modifier
.fillMaxSize(1f)
.align(Alignment.CenterHorizontally),
) {
CollectionFileItem(
collection = collection,
onBookmarkClick = { onBookmarkClick(collection.id) },
modifier =
Modifier
.align(Alignment.Center)
.noRippleClickable(
onClick = { onCollectionItemClick(collection.id) },
),
)
)
}
}
}
}
}

else -> {}
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error/Empty 상태가 빈 화면으로 끝납니다.
Loading/Success만 처리하고 나머지는 렌더링이 비어 있어, 실패/빈 상태에서 사용자에게 아무 피드백이 없습니다. 최소한 에러/빈 상태 UI나 재시도 경로를 제공해주세요.

🤖 Prompt for AI Agents
In
`@app/src/main/java/com/flint/presentation/collectionlist/CollectionListScreen.kt`
around lines 127 - 184, The when-block handling collectionList only covers
UiState.Loading and UiState.Success, leaving error/empty states rendering blank;
add explicit branches for UiState.Empty and UiState.Error inside the same
when(collectionList) to render a minimal Empty/Error UI (message + optional
icon) and a retry action (wire to an existing reload callback or add a new
onRetry lambda parameter to the screen if none exists), e.g., implement case
UiState.Empty { Centered text "No collections" + optional create button } and
case is UiState.Error { Centered text with error message and a Button that calls
onRetry() }, keeping the rest of the layout (LazyVerticalGrid,
CollectionFileItem, onCollectionItemClick, onBookmarkClick) unchanged.

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.

에러 관리 나중에 할테니까 답글 달지 말고 Resolve 누르렴

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.

🧩 Analysis chain

In
@app/src/main/java/com/flint/presentation/collectionlist/CollectionListScreen.kt
around lines 127 - 184, The when-block handling collectionList only covers
UiState.Loading and UiState.Success, leaving error/empty states rendering blank;
add explicit branches for UiState.Empty and UiState.Error inside the same
when(collectionList) to render a minimal Empty/Error UI (message + optional
icon) and a retry action (wire to an existing reload callback or add a new
onRetry lambda parameter to the screen if none exists), e.g., implement case
UiState.Empty { Centered text "No collections" + optional create button } and
case is UiState.Error { Centered text with error message and a Button that calls
onRetry() }, keeping the rest of the layout (LazyVerticalGrid,
CollectionFileItem, onCollectionItemClick, onBookmarkClick) unchanged.


</details>

<!-- fingerprinting:phantom:poseidon:eagle -->

<!-- This is an auto-generated comment by CodeRabbit -->' || true

# Try to resolve the thread using the pulls review-comment endpoint
gh api \
  --method PATCH \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  /repos/imflint/Flint-Android/pulls/149/reviews/2711375937 \
  -f event='COMMENT' || true

# List current review threads to understand the structure
gh api \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  /repos/imflint/Flint-Android/pulls/149/comments \
  --jq '.[] | select(.id == 2711375937) | {id, path, line, body}'

🏁 Script executed:

#!/bin/bash
# Resolve the review comment thread
gh api \
  --method PUT \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  /repos/imflint/Flint-Android/pulls/comments/2711375937 \
  -f body='_⚠️ Potential issue_ | _🟡 Minor_

**Error/Empty 상태가 빈 화면으로 끝납니다.**  
Loading/Success만 처리하고 나머지는 렌더링이 비어 있어, 실패/빈 상태에서 사용자에게 아무 피드백이 없습니다. 최소한 에러/빈 상태 UI나 재시도 경로를 제공해주세요.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Length of output: 68


🏁 Script executed:

#!/bin/bash
# Get the review thread ID and resolve it
gh api \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  /repos/imflint/Flint-Android/pulls/149/comments/2711375937 \
  --jq '{id, pull_request_review_id, path}'

# Resolve the review thread by marking it as resolved
gh api \
  --method PUT \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  /repos/imflint/Flint-Android/pulls/149/comments/2711375937/replies \
  -f body="Resolved as requested." 2>/dev/null || echo "Could not add reply"

# Try to resolve using the review thread endpoint
gh pr review 149 --repo imflint/Flint-Android --comment -b ""

Length of output: 670


@nahy-512 네, 알겠습니다! 👍


🧬 Code Graph Analysis Results

[Filename: app/src/main/java/com/flint/presentation/main/MainNavigator.kt, Lines 116-118]

fun navigateToCollectionList(routeType: CollectionListRouteType) {
        navController.navigateToCollectionList(routeType =  routeType)
    }

[Filename: app/src/main/java/com/flint/presentation/collectionlist/navigation/CollectionListNavigation.kt, Lines 12-22]

fun NavController.navigateToCollectionList(
    routeType: CollectionListRouteType,
    navOptions: NavOptions? = null
) {
    navigate(
        Route.CollectionList(
            routeType = routeType,
        ),
        navOptions,
    )
}

[Filename: app/src/main/java/com/flint/core/designsystem/theme/Theme.kt, Lines 8-16]

`@Composable`
fun FlintTheme(content: `@Composable` () -> Unit) {
    CompositionLocalProvider(
        LocalColors provides FlintColors,
        LocalTypography provides FlintTypography,
    ) {
        content()
    }
}

[Filename: app/src/main/java/com/flint/core/designsystem/component/toast/ShowToast.kt, Lines 25-50]

`@Composable`
fun ShowToast(
    text: String,
    imageVector: ImageVector?,
    paddingValues: PaddingValues,
    yOffset: Dp,
    hide: () -> Unit,
) {
    LaunchedEffect(Unit) {
        delay(2.seconds)
        hide()
    }

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.BottomCenter,
    ) {
        FlintToast(
            text = text,
            imageVector = imageVector,
            modifier = Modifier
                .padding(paddingValues)
                .padding(bottom = yOffset),
        )
    }
}

[Filename: app/src/main/java/com/flint/core/designsystem/component/toast/ShowSaveToast.kt, Lines 23-46]

`@Composable`
fun ShowSaveToast(
    navigateToSavedCollection: () -> Unit,
    paddingValues: PaddingValues,
    yOffset: Dp,
    hide: () -> Unit,
) {
    LaunchedEffect(Unit) {
        delay(2.seconds)
        hide()
    }

    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.BottomCenter,
    ) {
        SaveToast(
            navigateToSavedCollection = navigateToSavedCollection,
            modifier = Modifier
                .padding(paddingValues)
                .padding(bottom = yOffset),
        )
    }
}

[Filename: app/src/main/java/com/flint/presentation/collectionlist/component/CollectionFileItem.kt, Lines 39-84]

`@Composable`
fun CollectionFileItem(
    collection: CollectionItemModel,
    onBookmarkClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(12.dp),
        modifier = modifier,
    ) {
        with(collection) {
            CollectionFileContent(
                profileImageUrl = profileUrl.orEmpty(),
                nickname = nickname,
                isBookmarked = isBookmarked,
                bookmarkCount = bookmarkCount,
                poster1Url = imageList.getOrElse(0) { "" },
                poster2Url = imageList.getOrElse(1) { "" },
                onBookmarkClick = onBookmarkClick,
                modifier = Modifier.size(154.dp),
            )
        }

        Column(
            verticalArrangement = Arrangement.spacedBy(4.dp),
            modifier = Modifier.width(154.dp),
        ) {
            Text(
                text = collection.title,
                style = FlintTheme.typography.body1M16,
                color = FlintTheme.colors.white,
                maxLines = 2,
                overflow = TextOverflow.Ellipsis,
            )

            Text(
                text = collection.description,
                style = FlintTheme.typography.caption1R12,
                color = FlintTheme.colors.gray300,
                maxLines = 2,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.height(45.dp),
            )
        }
    }
}

[Filename: app/src/main/java/com/flint/core/designsystem/component/topappbar/FlintBackTopAppbar.kt, Lines 16-59]

`@Composable`
fun FlintBackTopAppbar(
    // 뒤로가기 버튼이 있는 이상 뒤로 움직이는 이벤트는 무조건 있음. 그래서 필수 인자
    onClick: () -> Unit,
    // 밖에서 주입해주기 위해 / 책임을 밖으로 빼기 위해
    modifier: Modifier = Modifier,
    backgroundColor: Color = FlintTheme.colors.background,
    title: String = "",
    closeable: Boolean = false,
    actionText: String = "",
    onActionClick: () -> Unit = {},
    textColor: Color = Color.Unspecified,
) {
    FlintBasicTopAppbar(
        modifier = modifier,
        backgroundColor = backgroundColor,
        navigationIcon = {
            Icon(
                modifier = Modifier.noRippleClickable(onClick = onClick),
                imageVector = ImageVector.vectorResource(R.drawable.ic_back),
                contentDescription = null,
                tint = FlintTheme.colors.white,
            )
        },
        title = title,
        action = {
            if (closeable) {
                Icon(
                    modifier = Modifier.noRippleClickable(onClick = onActionClick),
                    imageVector = ImageVector.vectorResource(R.drawable.ic_cancel),
                    contentDescription = null,
                    tint = FlintTheme.colors.white,
                )
            } else {
                Text(
                    modifier = Modifier.noRippleClickable(onClick = onActionClick),
                    text = actionText,
                    color = textColor,
                    style = FlintTheme.typography.body1M16,
                )
            }
        },
    )
}

🧠 Learnings used
Learnt from: giovannijunseokim
Repo: imflint/Flint-Android PR: 72
File: app/src/main/java/com/flint/core/designsystem/component/collection/PeopleBottomSheet.kt:85-98
Timestamp: 2026-01-13T17:03:25.553Z
Learning: In PeopleBottomSheet.kt, the `people` list passed to LazyColumn is static and does not change while the bottom sheet is visible, so a key parameter is not required for the items() call.

Learnt from: chanmi1125
Repo: imflint/Flint-Android PR: 77
File: app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt:45-72
Timestamp: 2026-01-13T19:02:48.312Z
Learning: In `app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateThumbnail.kt`, the height difference between CollectionCreateEmptyThumbnail (no aspectRatio) and CollectionCreateFillThumbnail (aspectRatio 1.5f / 1f) is intentional design - the empty state should not have the same aspectRatio as the filled state.

Comment on lines +71 to +90
val updatedCollections = collectionList.data.collections.map { collection ->
if (collection.id == collectionId) {
collection.copy(
isBookmarked = isBookmarked,
bookmarkCount = if (isBookmarked) {
collection.bookmarkCount + 1
} else {
collection.bookmarkCount - 1
}
)
} else {
collection
}
}.toImmutableList()

currentState.copy(
collectionList = UiState.Success(
CollectionListModel(collections = updatedCollections)
)
)
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

SAVED 목록에서 언북마크 시 항목 제거가 필요합니다.

현재 로직은 SAVED 화면에서도 항목을 단순 토글만 해서 “저장 목록”에 미저장 항목이 남을 수 있어요. 또한 bookmarkCount가 0일 때 감소하면 음수가 될 수 있습니다. 아래처럼 SAVED에서는 언북마크 시 제거하고, 카운트는 0 하한을 두는 방식을 권장합니다.

🛠️ 수정 제안
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
+import kotlin.math.max
@@
-                        val updatedCollections = collectionList.data.collections.map { collection ->
-                            if (collection.id == collectionId) {
-                                collection.copy(
-                                    isBookmarked = isBookmarked,
-                                    bookmarkCount = if (isBookmarked) {
-                                        collection.bookmarkCount + 1
-                                    } else {
-                                        collection.bookmarkCount - 1
-                                    }
-                                )
-                            } else {
-                                collection
-                            }
-                        }.toImmutableList()
+                        val updatedCollections =
+                            when (routeType) {
+                                CollectionListRouteType.SAVED ->
+                                    collectionList.data.collections.mapNotNull { collection ->
+                                        if (collection.id == collectionId) {
+                                            if (!isBookmarked) null
+                                            else collection.copy(
+                                                isBookmarked = true,
+                                                bookmarkCount = max(0, collection.bookmarkCount + 1),
+                                            )
+                                        } else {
+                                            collection
+                                        }
+                                    }.toImmutableList()
+                                else ->
+                                    collectionList.data.collections.map { collection ->
+                                        if (collection.id == collectionId) {
+                                            val nextCount =
+                                                max(
+                                                    0,
+                                                    collection.bookmarkCount + if (isBookmarked) 1 else -1,
+                                                )
+                                            collection.copy(
+                                                isBookmarked = isBookmarked,
+                                                bookmarkCount = nextCount,
+                                            )
+                                        } else {
+                                            collection
+                                        }
+                                    }.toImmutableList()
+                            }
📝 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.

Suggested change
val updatedCollections = collectionList.data.collections.map { collection ->
if (collection.id == collectionId) {
collection.copy(
isBookmarked = isBookmarked,
bookmarkCount = if (isBookmarked) {
collection.bookmarkCount + 1
} else {
collection.bookmarkCount - 1
}
)
} else {
collection
}
}.toImmutableList()
currentState.copy(
collectionList = UiState.Success(
CollectionListModel(collections = updatedCollections)
)
)
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.math.max
val updatedCollections =
when (routeType) {
CollectionListRouteType.SAVED ->
collectionList.data.collections.mapNotNull { collection ->
if (collection.id == collectionId) {
if (!isBookmarked) null
else collection.copy(
isBookmarked = true,
bookmarkCount = max(0, collection.bookmarkCount + 1),
)
} else {
collection
}
}.toImmutableList()
else ->
collectionList.data.collections.map { collection ->
if (collection.id == collectionId) {
val nextCount =
max(
0,
collection.bookmarkCount + if (isBookmarked) 1 else -1,
)
collection.copy(
isBookmarked = isBookmarked,
bookmarkCount = nextCount,
)
} else {
collection
}
}.toImmutableList()
}
currentState.copy(
collectionList = UiState.Success(
CollectionListModel(collections = updatedCollections)
)
)
🤖 Prompt for AI Agents
In
`@app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt`
around lines 71 - 90, When toggling bookmark state in CollectionListViewModel
(handling collectionList -> updatedCollections), ensure that on the SAVED screen
you remove the item when it is being un-bookmarked instead of leaving a toggled
item: detect the SAVED context (your existing screen/state flag) and for
collection.id == collectionId, if isBookmarked == false and current view is
SAVED exclude that collection from the resulting list; for all updates clamp
bookmarkCount to a non-negative value (e.g., bookmarkCount = max(0,
collection.bookmarkCount + if (isBookmarked) 1 else -1)). Update the code that
builds updatedCollections and the UiState.Success(CollectionListModel(...))
accordingly.

Copy link
Copy Markdown
Contributor

@giovannijunseokim giovannijunseokim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 💪

}
}

fun toggleCollectionBookmark(collectionId: String) {
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.

p3: 요거 관해서 Throttle + Optimistic UI 구현해둔게 있는데 궁금하시면 CollectionDetailViewModel 참고해주세요! 👍

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.

넵 안그래도 작업해주셨다고 해서 보고 추가로 저도 적용해봐야지 했는데ㅎㅎ 확인해보겠습니다 감사합니다!

@nahy-512 nahy-512 merged commit e005a23 into develop Jan 21, 2026
1 check passed
@nahy-512 nahy-512 deleted the feat/#148-collection-list-api branch January 21, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 컬렉션 목록 화면 API 연동

2 participants