Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/src/main/java/com/flint/core/designsystem/theme/Color.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ data class Colors(
val blue: Color,
val kakao: Color,
val buttonStroke: Brush,
val myGradient: Brush,
)

val FlintColors =
Expand Down Expand Up @@ -163,6 +164,10 @@ val FlintColors =
Brush.verticalGradient(
colors = listOf(Color(0xFFAEAEAE), Color(0xFF666666)),
),
myGradient =
Brush.verticalGradient(
colors = listOf(Color(0xFF424BBD).copy(1f), Color(0xFF121212).copy(alpha = 0.04f)),
),
)

@Preview(device = Devices.DESKTOP)
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/com/flint/core/navigation/Route.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.flint.core.navigation

import com.flint.domain.type.CollectionListRouteType
import kotlinx.serialization.Serializable

interface Route {
Expand All @@ -24,7 +25,9 @@ interface Route {
data object OnboardingDone : Route

@Serializable
data object CollectionList : Route
data class CollectionList(
val routeType: CollectionListRouteType,
) : Route

@Serializable
data class CollectionDetail(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,84 +36,6 @@ data class CollectionDetailModelNew(
)
}

data class CollectionDetailModel(
val collectionId: String,
val collectionTitle: String,
val collectionContent: String,
val collectionImageUrl1: String,
val collectionImageUrl2: String,
val createdAt: String,
val isBookmarked: Boolean,
val bookmarkCount: Int,
val author: AuthorModel,
) {
companion object {
val Fake =
CollectionDetailModel(
collectionId = "id_0",
collectionTitle = "컬렉션 제목 1",
collectionContent = "컬렉션에 대한 설명이 들어갑니다. 최대 2줄까지 표시됩니다.",
collectionImageUrl1 = "",
collectionImageUrl2 = "",
createdAt = "2024-01-01",
isBookmarked = true,
bookmarkCount = 10,
author = AuthorModel.Companion.Fake,
)

val FakeList = persistentListOf(
Fake.copy(
collectionId = "1",
collectionTitle = "2024 인생 영화 모음",
collectionContent = "올해 본 영화 중 최고만 모았습니다",
isBookmarked = true,
bookmarkCount = 128,
author = AuthorModel.Fake.copy(nickname = "닉네임은여덟글자"),
),
Fake.copy(
collectionId = "2",
collectionTitle = "혼자 보기 아까운 숨은 명작",
collectionContent = "알려지지 않은 갓작들",
isBookmarked = false,
bookmarkCount = 56,
author = AuthorModel.Fake.copy(nickname = "시네필"),
),
Fake.copy(
collectionId = "3",
collectionTitle = "비 오는 날 보기 좋은 영화",
collectionContent = "감성 충전용 영화 컬렉션입니다. 우울할 때 보면 좋아요.",
isBookmarked = true,
bookmarkCount = 234,
author = AuthorModel.Fake.copy(nickname = "무비러버"),
),
Fake.copy(
collectionId = "4",
collectionTitle = "넷플릭스 정주행 리스트",
collectionContent = "주말에 몰아보기 좋은 시리즈",
isBookmarked = false,
bookmarkCount = 89,
author = AuthorModel.Fake,
),
Fake.copy(
collectionId = "5",
collectionTitle = "SF 덕후 추천작",
collectionContent = "우주와 미래를 다룬 SF 명작 모음",
isBookmarked = true,
bookmarkCount = 412,
author = AuthorModel.Fake.copy(nickname = "SF매니아"),
),
Fake.copy(
collectionId = "6",
collectionTitle = "첫 데이트용 무난한 영화",
collectionContent = "어색한 분위기를 풀어줄 영화들",
isBookmarked = false,
bookmarkCount = 67,
author = AuthorModel.Fake.copy(nickname = "연애고수"),
),
)
}
}

private fun CollectionDetailResponseDto.Author.toModel(): AuthorModelNew {
return AuthorModelNew(
id = id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.flint.domain.type

enum class CollectionListRouteType(
val title: String
) {
CREATED(title = "전체 컬렉션"),
SAVED(title = "저장 컬렉션"),
RECENT(title = "눈여겨보고 있는 컬렉션"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,14 @@ import com.flint.presentation.collectiondetail.sideeffect.CollectionDetailSideEf
import com.flint.presentation.collectiondetail.uistate.CollectionDetailUiState
import com.flint.core.designsystem.component.toast.ShowSaveToast
import com.flint.core.designsystem.component.toast.ShowToast
import com.flint.domain.type.CollectionListRouteType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

@Composable
fun CollectionDetailRoute(
paddingValues: PaddingValues,
navigateToCollectionList: () -> Unit,
navigateToCollectionList: (CollectionListRouteType) -> Unit,
navigateUp: () -> Unit,
viewModel: CollectionDetailViewModel = hiltViewModel(),
) {
Expand Down Expand Up @@ -134,7 +135,7 @@ fun CollectionDetailRoute(
if (showSaveToast) {
ShowSaveToast(
navigateToSavedCollection = {
navigateToCollectionList()
navigateToCollectionList(CollectionListRouteType.SAVED)
},
paddingValues = paddingValues,
yOffset = 12.dp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.flint.core.navigation.Route
import com.flint.domain.type.CollectionListRouteType
import com.flint.presentation.collectiondetail.CollectionDetailRoute

fun NavController.navigateToCollectionDetail(
Expand All @@ -17,7 +18,7 @@ fun NavController.navigateToCollectionDetail(

fun NavGraphBuilder.collectionDetailNavGraph(
paddingValues: PaddingValues,
navigateToCollectionList: () -> Unit,
navigateToCollectionList: (CollectionListRouteType) -> Unit,
navigateUp: () -> Unit,
) {
composable<Route.CollectionDetail> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,44 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flint.core.common.extension.noRippleClickable
import com.flint.core.common.util.UiState
import com.flint.core.designsystem.component.indicator.FlintLoadingIndicator
import com.flint.core.designsystem.component.topappbar.FlintBackTopAppbar
import com.flint.core.designsystem.theme.FlintTheme
import com.flint.domain.model.collection.CollectionDetailModel
import com.flint.domain.model.collection.CollectionListModel
import com.flint.presentation.collectionlist.component.CollectionFileItem
import kotlinx.collections.immutable.ImmutableList

@Composable
fun CollectionListRoute(
paddingValues: PaddingValues,
navigateUp: () -> Unit,
navigateToCollectionDetail: (collectionId: String) -> Unit,
viewModel: CollectionListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

CollectionListScreen(
modifier = Modifier.padding(paddingValues),
title = "전체 컬렉션", // TODO: 타이틀 변경 필요
title = viewModel.routeType.title,
onBackClick = navigateUp,
onCollectionItemClick = navigateToCollectionDetail,
collections = CollectionDetailModel.FakeList, // TODO: 변경 필요
collectionList = uiState.collectionList,
)
}

@Composable
private fun CollectionListScreen(
onBackClick: () -> Unit,
title: String,
collections: ImmutableList<CollectionDetailModel>,
collectionList: UiState<CollectionListModel>,
onCollectionItemClick: (collectionId: String) -> Unit,
modifier: Modifier = Modifier,
) {
Expand All @@ -63,42 +70,64 @@ private fun CollectionListScreen(

Spacer(Modifier.height(12.dp))

Text(
text = "총 ${collections.size}개",
color = FlintTheme.colors.gray100,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
when (collectionList) {
is UiState.Loading -> {
FlintLoadingIndicator()
}

is UiState.Success -> {
with(collectionList.data) {
Text(
text = "총 ${collections.size}개",
color = FlintTheme.colors.gray100,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)

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

}

else -> {}
}
Comment on lines +127 to 184
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.

}
}
Expand All @@ -110,7 +139,7 @@ private fun CollectionListScreenPreview() {
CollectionListScreen(
onBackClick = {},
title = "전체 컬렉션",
collections = CollectionDetailModel.FakeList,
collectionList = UiState.Success(CollectionListModel.FakeList),
onCollectionItemClick = {}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.flint.presentation.collectionlist

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.flint.core.common.util.UiState
import com.flint.core.navigation.Route
import com.flint.domain.repository.CollectionRepository
import com.flint.domain.repository.UserRepository
import com.flint.domain.type.CollectionListRouteType
import com.flint.presentation.collectionlist.uistate.CollectionListUiState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class CollectionListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
val userRepository: UserRepository,
val collectionRepository: CollectionRepository,
) : ViewModel() {

val routeType = savedStateHandle.toRoute<Route.CollectionList>().routeType

private val _uiState = MutableStateFlow<CollectionListUiState>(CollectionListUiState())
val uiState: MutableStateFlow<CollectionListUiState> = _uiState
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

init {
getCollectionList()
}

private fun getCollectionList() {
viewModelScope.launch {
_uiState.update { it.copy(collectionList = UiState.Loading) }

when (routeType) {
CollectionListRouteType.CREATED -> userRepository.getUserCreatedCollections(userId = null)
CollectionListRouteType.SAVED -> userRepository.getUserBookmarkedCollections(userId = null)
CollectionListRouteType.RECENT -> collectionRepository.getRecentCollectionList()
}.onSuccess { result ->
_uiState.update { it.copy(collectionList = UiState.Success(result)) }
}.onFailure {
_uiState.update { it.copy(collectionList = UiState.Failure) }
}
}
}
}
Loading