From 43d6eb54909ec24be4972f336a9152dbc5a10a2a Mon Sep 17 00:00:00 2001 From: kimjw Date: Thu, 10 Sep 2026 22:44:53 +0900 Subject: [PATCH 1/5] =?UTF-8?q?design:=20=ED=99=88=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=BB=AC=EB=A0=89=EC=85=98=20=EC=B9=B4=EB=93=9C=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=EA=B3=BC=20=EA=B7=B8=EB=9D=BC?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=EC=85=98=EC=9D=84=20=ED=94=BC=EA=B7=B8?= =?UTF-8?q?=EB=A7=88=20=EC=8A=A4=ED=8E=99=EC=97=90=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA [3] home 플리너 컬렉션 ui 반영 - 배지/제목/소개글을 하단 기준에서 상단 기준 배치로 변경하여 소개글이 한 줄인 컬렉션에서 텍스트 블록이 아래로 밀리던 문제 해결 - 배지 위치 170~202dp -> 178~210dp, 배지와 제목 간격 24dp -> 16dp - 하단 그라데이션 높이 34dp -> 42dp, 시작 stop 0% -> 26%, 축 1.15배 적용 - 비선택 카드 하단 그라데이션을 #21242C 0.35 -> #2D4254 1.0 으로 교체 팔레트에 없는 색이라 gray800Gradient 를 cardShadeGradient 로 이름 변경 중간 그라데이션(70~202dp)은 피그마 값과 코드가 동일하여 현행 유지, 디자이너 수치 확정 후 별도 반영 예정 Co-Authored-By: Claude Opus 5 (1M context) --- .../android/core/designsystem/theme/Color.kt | 30 +++-- .../home/component/RecommendCollectionCard.kt | 114 +++++++++++++----- 2 files changed, 104 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/flint/android/core/designsystem/theme/Color.kt b/app/src/main/java/com/flint/android/core/designsystem/theme/Color.kt index b628e792..a017922b 100644 --- a/app/src/main/java/com/flint/android/core/designsystem/theme/Color.kt +++ b/app/src/main/java/com/flint/android/core/designsystem/theme/Color.kt @@ -79,7 +79,7 @@ data class Colors( val blueGradient: Brush, val primary400Gradient: Brush, val grayGradient: Brush, - val gray800Gradient: Brush, + val cardShadeGradient: Brush, val navbarGradient: Brush, val thumbnailGradient: Brush, val userBadgeGradient: Brush, @@ -184,18 +184,30 @@ val FlintColors = Brush.verticalGradient( colors = listOf(Color(0xFF062845).copy(alpha = 0f), Color(0xFF062845).copy(1f)), ), - primary400Gradient = - Brush.verticalGradient( - colors = listOf(Color(0xFF1ABFF2).copy(0f), Color(0xFF1ABFF2).copy(0.35f)), - ), + primary400Gradient = object : ShaderBrush() { + override fun createShader(size: Size): Shader { + return LinearGradientShader( + from = Offset(size.width * 0.5f, 0f), + to = Offset(size.width * 0.5f, size.height * 1.15f), + colors = listOf(Color(0xFF1ABFF2).copy(alpha = 0f), Color(0xFF1ABFF2).copy(alpha = 0.35f)), + colorStops = listOf(0.26f, 1f), + ) + } + }, grayGradient = Brush.verticalGradient( colors = listOf(Color(0xFF21242C).copy(alpha = 0f), Color(0xFF21242C).copy(alpha = 1f)), ), - gray800Gradient = - Brush.verticalGradient( - colors = listOf(Color(0xFF21242C).copy(alpha = 0f), Color(0xFF21242C).copy(alpha = 0.35f)), - ), + cardShadeGradient = object : ShaderBrush() { + override fun createShader(size: Size): Shader { + return LinearGradientShader( + from = Offset(size.width * 0.5f, 0f), + to = Offset(size.width * 0.5f, size.height * 1.15f), + colors = listOf(Color(0xFF2D4254).copy(alpha = 0f), Color(0xFF2D4254)), + colorStops = listOf(0.26f, 1f), + ) + } + }, navbarGradient = Brush.verticalGradient( colors = listOf(Color(0xFF121212).copy(alpha = 1f), Color(0xFF121212).copy(alpha = 0f)), diff --git a/app/src/main/java/com/flint/android/presentation/home/component/RecommendCollectionCard.kt b/app/src/main/java/com/flint/android/presentation/home/component/RecommendCollectionCard.kt index cce6a67d..876b7c68 100644 --- a/app/src/main/java/com/flint/android/presentation/home/component/RecommendCollectionCard.kt +++ b/app/src/main/java/com/flint/android/presentation/home/component/RecommendCollectionCard.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -30,6 +31,14 @@ import com.flint.android.core.designsystem.component.image.ProfileImage import com.flint.android.core.designsystem.theme.FlintTheme import com.flint.android.domain.model.collection.CollectionItemModel +private val THUMBNAIL_HEIGHT = 202.dp +private val BADGE_HEIGHT = 32.dp + +/** 배지가 썸네일 위로 겹쳐 올라가는 높이. 배지는 178~210dp 구간에 놓인다. */ +private val BADGE_OVERLAP = 24.dp +private val BADGE_TO_TITLE_SPACING = 16.dp +private val BOTTOM_GRADIENT_HEIGHT = 42.dp + @Composable fun RecommendCollectionCard( item: CollectionItemModel, @@ -39,7 +48,7 @@ fun RecommendCollectionCard( ) { val backgroundColor = if (isCurrentPage) FlintTheme.colors.primary900 else FlintTheme.colors.gray800 val midGradient = if (isCurrentPage) FlintTheme.colors.blueGradient else FlintTheme.colors.grayGradient - val bottomGradient = if (isCurrentPage) FlintTheme.colors.primary400Gradient else FlintTheme.colors.gray800Gradient + val bottomGradient = if (isCurrentPage) FlintTheme.colors.primary400Gradient else FlintTheme.colors.cardShadeGradient Box( modifier = modifier @@ -55,7 +64,7 @@ fun RecommendCollectionCard( contentScale = ContentScale.Crop, modifier = Modifier .fillMaxWidth() - .height(202.dp), + .height(THUMBNAIL_HEIGHT), ) Box( @@ -66,38 +75,40 @@ fun RecommendCollectionCard( .background(midGradient), ) - Row( - modifier = Modifier - .padding(top = 52.dp) - .height(32.dp) - .clip(RoundedCornerShape(16.dp)) - .background(brush = FlintTheme.colors.userBadgeGradient) - .border(width = 0.5.dp, brush = FlintTheme.colors.userBadgeStroke, shape = RoundedCornerShape(16.dp)) - .padding(top = 4.dp, bottom = 4.dp, start = 6.dp, end = 8.dp) - .align(Alignment.Center), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - ProfileImage( - imageUrl = item.profileUrl, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - Text( - text = item.nickname, - style = FlintTheme.typography.caption1R12, - color = FlintTheme.colors.gray200, - maxLines = 1, - ) - } - + // 배지가 썸네일 하단에 걸치도록 상단 기준으로 배치한다. + // 하단 기준으로 두면 제목/소개글이 한 줄일 때 텍스트 블록 전체가 아래로 밀린다. Column( modifier = Modifier .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(bottom = 35.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), + .align(Alignment.TopCenter) + .padding(top = THUMBNAIL_HEIGHT - BADGE_OVERLAP), + horizontalAlignment = Alignment.CenterHorizontally, ) { + Row( + modifier = Modifier + .height(BADGE_HEIGHT) + .clip(RoundedCornerShape(16.dp)) + .background(brush = FlintTheme.colors.userBadgeGradient) + .border(width = 0.5.dp, brush = FlintTheme.colors.userBadgeStroke, shape = RoundedCornerShape(16.dp)) + .padding(top = 4.dp, bottom = 4.dp, start = 6.dp, end = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + ProfileImage( + imageUrl = item.profileUrl, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + Text( + text = item.nickname, + style = FlintTheme.typography.caption1R12, + color = FlintTheme.colors.gray200, + maxLines = 1, + ) + } + + Spacer(Modifier.height(BADGE_TO_TITLE_SPACING)) + Text( text = item.title, style = FlintTheme.typography.head3Sb18, @@ -109,6 +120,9 @@ fun RecommendCollectionCard( .fillMaxWidth() .padding(horizontal = 48.dp) ) + + Spacer(Modifier.height(4.dp)) + Text( text = item.description, style = FlintTheme.typography.caption1R12, @@ -125,7 +139,7 @@ fun RecommendCollectionCard( Box( modifier = Modifier .fillMaxWidth() - .height(34.dp) + .height(BOTTOM_GRADIENT_HEIGHT) .align(Alignment.BottomCenter) .background(bottomGradient) ) @@ -150,3 +164,41 @@ private fun RecommendCollectionCardPreview() { ) } } + +@Preview +@Composable +private fun RecommendCollectionCardInactivePreview() { + FlintTheme { + RecommendCollectionCard( + item = CollectionItemModel( + id = "3", + thumbnailUrl = null, + title = "사랑에 빠지기 10초 전", + description = "시간이 흘러도 빛이 바래지 않는,사랑의 미묘한 온도를 담은 제 최애 영화 모음집입니다", + nickname = "얀비", + profileUrl = null + ), + isCurrentPage = false, + onItemClick = {} + ) + } +} + +@Preview +@Composable +private fun RecommendCollectionCardSingleLinePreview() { + FlintTheme { + RecommendCollectionCard( + item = CollectionItemModel( + id = "2", + thumbnailUrl = null, + title = "사랑에 빠지기 10초 전", + description = "한 줄짜리 소개글입니다", + nickname = "얀비", + profileUrl = null + ), + isCurrentPage = true, + onItemClick = {} + ) + } +} From 2c71aa4aba8e6680d7dc7a2c6c21e49953972fa8 Mon Sep 17 00:00:00 2001 From: kimjw Date: Thu, 10 Sep 2026 23:13:37 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=ED=83=90=EC=83=89=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=A7=84=EC=9E=85=ED=95=9C=20=EC=BB=AC=EB=A0=89=EC=85=98?= =?UTF-8?q?=EC=9D=98=20=ED=83=80=EA=B2=9F=20=EC=9E=91=ED=92=88=EC=9D=84=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=A4=91=EC=95=99=EC=97=90=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA [5] explore 탐색 반영 animateScrollToItem 은 아이템 상단을 뷰포트 상단에 붙이기 때문에 탐색에서 선택한 작품이 화면 맨 위에 걸려 있었다. 아이템 높이는 배치 전에 알 수 없어, 상단 정렬로 스크롤해 크기를 측정한 뒤 중앙까지 남은 거리만큼 추가로 이동하도록 animateScrollToCenteredItem 을 추가했다. - 상단 정렬을 1단계로 둔 이유는 보정 거리가 (뷰포트 - 아이템)/2 로 가장 짧기 때문 - 아이템이 뷰포트보다 크면 윗부분이 잘리므로 상단 정렬 유지 - 리스트 끝 근처는 animateScrollBy 가 클램프하므로 별도 처리 불필요 Co-Authored-By: Claude Opus 5 (1M context) --- .../CollectionDetailScreen.kt | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/flint/android/presentation/collectiondetail/CollectionDetailScreen.kt b/app/src/main/java/com/flint/android/presentation/collectiondetail/CollectionDetailScreen.kt index fd628095..65a48c32 100644 --- a/app/src/main/java/com/flint/android/presentation/collectiondetail/CollectionDetailScreen.kt +++ b/app/src/main/java/com/flint/android/presentation/collectiondetail/CollectionDetailScreen.kt @@ -2,6 +2,7 @@ package com.flint.android.presentation.collectiondetail import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -287,7 +289,7 @@ fun CollectionDetailScreen( val targetIndex: Int = contents.indexOfFirst { it.imageUrl == targetImageUrl } if (targetIndex == -1) return@LaunchedEffect - lazyListState.animateScrollToItem(CONTENT_LIST_HEADER_ITEM_COUNT + targetIndex) + lazyListState.animateScrollToCenteredItem(CONTENT_LIST_HEADER_ITEM_COUNT + targetIndex) } if (showPeopleBottomSheet) { @@ -384,6 +386,26 @@ fun CollectionDetailScreen( } } +/** + * [index] 아이템이 뷰포트 중앙에 오도록 스크롤한다. + * + * 아이템 높이는 실제로 배치되기 전에는 알 수 없으므로, 먼저 상단 정렬로 스크롤해 + * 아이템을 화면에 올린 뒤 중앙까지 남은 거리만큼 추가로 이동한다. + * 아이템이 뷰포트보다 크면 중앙에 맞출 때 윗부분이 잘려 나가므로 상단 정렬을 유지한다. + */ +private suspend fun LazyListState.animateScrollToCenteredItem(index: Int) { + animateScrollToItem(index) + + val item: LazyListItemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index } ?: return + val viewportStart: Int = layoutInfo.viewportStartOffset + val viewportEnd: Int = layoutInfo.viewportEndOffset + if (item.size >= viewportEnd - viewportStart) return + + val itemCenter: Int = item.offset + item.size / 2 + val viewportCenter: Int = (viewportStart + viewportEnd) / 2 + animateScrollBy((itemCenter - viewportCenter).toFloat()) +} + private data class ScreenPreviewData( val thumbnailUrl: String, val title: String, From 56ff7749daaa1c8b3ad183780b44c0d4b2de1944 Mon Sep 17 00:00:00 2001 From: kimjw Date: Thu, 10 Sep 2026 23:17:45 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=EC=95=BD=EA=B4=80=20=EC=A0=84?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20=EB=85=B8=EC=B6=9C=EB=90=98=EB=8D=98=20asi?= =?UTF-8?q?de=20=ED=83=9C=EA=B7=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA [1-1] Tos 개인정보처리방침 반영 서버 약관 API 가 내려주는 개인정보 처리방침 본문에 Notion 콜아웃 블록의 태그가 텍스트로 남아 있어 개인정보 보호책임자 안내 위아래에 그대로 노출됐다. parseTermsMarkdown 이 한 줄 전체가 HTML 태그인 줄을 걸러내도록 했다. -