From 4c13ce618adb81a85dd424e839794d14eb98a870 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Fri, 30 Jan 2026 23:48:45 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EC=95=A0=EC=85=8B=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 디자인 시스템에 사용될 신규 아이콘 4종을 추가했습니다. * `ic_storage` 추가 * `ic_lock` 추가 * `ic_setting` 추가 * `ic_edit` 추가 --- .../src/main/res/drawable/core_designsystem_ic_edit.xml | 9 +++++++++ .../src/main/res/drawable/core_designsystem_ic_lock.xml | 9 +++++++++ .../main/res/drawable/core_designsystem_ic_setting.xml | 9 +++++++++ .../main/res/drawable/core_designsystem_ic_storage.xml | 9 +++++++++ 4 files changed, 36 insertions(+) create mode 100644 Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_edit.xml create mode 100644 Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_lock.xml create mode 100644 Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_setting.xml create mode 100644 Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_storage.xml diff --git a/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_edit.xml b/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_edit.xml new file mode 100644 index 0000000..db98b7d --- /dev/null +++ b/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_lock.xml b/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_lock.xml new file mode 100644 index 0000000..5fc1d5c --- /dev/null +++ b/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_lock.xml @@ -0,0 +1,9 @@ + + + diff --git a/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_setting.xml b/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_setting.xml new file mode 100644 index 0000000..b609241 --- /dev/null +++ b/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_setting.xml @@ -0,0 +1,9 @@ + + + diff --git a/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_storage.xml b/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_storage.xml new file mode 100644 index 0000000..9182a2d --- /dev/null +++ b/Prezel/core/designsystem/src/main/res/drawable/core_designsystem_ic_storage.xml @@ -0,0 +1,9 @@ + + + From 61188700819ff659d43d3cde8b79a8664ea5eceb Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Sat, 31 Jan 2026 01:23:13 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20PrezelTabs=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 디자인 시스템에 맞춰 탭과 뷰페이저가 결합된 `PrezelTabs` 컴포넌트를 추가했습니다. * `PrezelTabItem` 데이터 클래스 및 `PrezelTabSize` (Small, Regular) 추가 * `SecondaryTabRow` 및 `HorizontalPager`를 이용한 탭 시스템 구현 * 탭 선택 시 애니메이션 또는 즉시 이동 로직 구현 * 배지(Badge) 표시 기능 지원 * 미리보기(Preview) 코드 추가 --- .../core/designsystem/component/PrezelTabs.kt | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt new file mode 100644 index 0000000..20f8788 --- /dev/null +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt @@ -0,0 +1,214 @@ +package com.team.prezel.core.designsystem.component + +import androidx.compose.foundation.background +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 +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.SecondaryTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.team.prezel.core.designsystem.foundation.typography.PrezelTextStyles +import com.team.prezel.core.designsystem.preview.ThemePreview +import com.team.prezel.core.designsystem.theme.PrezelTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlin.math.abs + +@Immutable +data class PrezelTabItem( + val label: String, + val badgeCount: Int? = null, + val enabled: Boolean = true, +) + +enum class PrezelTabSize { Small, Regular } + +@Composable +fun PrezelTabs( + tabs: ImmutableList, + pagerState: PagerState, + modifier: Modifier = Modifier, + size: PrezelTabSize = PrezelTabSize.Regular, + userScrolledEnabled: Boolean = true, + content: @Composable (pageIndex: Int) -> Unit, +) { + require(tabs.isNotEmpty()) { "tabs는 비어있을 수 없습니다." } + require(pagerState.pageCount == tabs.size) { + "pagerState.pageCount(${pagerState.pageCount})와 tabs.size(${tabs.size})가 일치해야 합니다." + } + + val scope = rememberCoroutineScope() + + Column(modifier = modifier.fillMaxSize()) { + SecondaryTabRow( + selectedTabIndex = pagerState.currentPage, + modifier = Modifier.fillMaxWidth(), + containerColor = Color.Transparent, + indicator = { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + .tabIndicatorOffset( + selectedTabIndex = pagerState.currentPage, + ).background(PrezelTheme.colors.solidBlack), + ) + }, + ) { + tabs.forEachIndexed { index, item -> + Tab( + selected = pagerState.currentPage == index, + onClick = { + if (!item.enabled) return@Tab + handleTabClick(scope, pagerState, index) + }, + modifier = Modifier.height(if (size == PrezelTabSize.Small) 36.dp else 48.dp), + enabled = item.enabled, + text = { + PrezelTabLabel( + label = item.label, + size = size, + badgeCount = item.badgeCount, + selected = pagerState.currentPage == index, + ) + }, + selectedContentColor = PrezelTheme.colors.solidBlack, + unselectedContentColor = PrezelTheme.colors.textDisabled, + ) + } + } + + HorizontalPager( + state = pagerState, + userScrollEnabled = userScrolledEnabled, + overscrollEffect = null, + ) { page -> + content(page) + } + } +} + +@Composable +private fun PrezelTabLabel( + label: String, + size: PrezelTabSize, + selected: Boolean, + badgeCount: Int? = null, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = label, + style = if (size == PrezelTabSize.Small) PrezelTextStyles.Body3Medium.toTextStyle() else PrezelTextStyles.Body2Bold.toTextStyle(), + ) + + if (badgeCount != null) { + PrezelBadge( + active = false, + count = badgeCount, + size = PrezelBadgeSize.REGULAR, + disabled = !selected, + ) + } + } +} + +private fun handleTabClick( + scope: CoroutineScope, + pagerState: PagerState, + target: Int, +) { + scope.launch { + val current = pagerState.currentPage + val distance = abs(current - target) + + if (distance <= 1) { + pagerState.animateScrollToPage(target) + } else { + pagerState.scrollToPage(target) + } + } +} + +@ThemePreview +@Composable +private fun PrezelMediumTabPreview() { + val tabs = persistentListOf( + PrezelTabItem(label = "Label", badgeCount = 0), + PrezelTabItem(label = "Label", badgeCount = 0), + PrezelTabItem(label = "Label", badgeCount = 0), + ) + val pagerState = rememberPagerState(initialPage = 0) { tabs.size } + + PrezelTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(PrezelTheme.colors.bgRegular), + ) { + PrezelTabs( + tabs = tabs, + pagerState = pagerState, + size = PrezelTabSize.Regular, + modifier = Modifier, + ) { page -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text("Page: $page") + } + } + } + } +} + +@ThemePreview +@Composable +private fun PrezelSmallTabPreview() { + val tabs = persistentListOf( + PrezelTabItem(label = "Label", badgeCount = 2), + PrezelTabItem(label = "Label", badgeCount = 3), + ) + val pagerState = rememberPagerState(initialPage = 0) { tabs.size } + + PrezelTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(PrezelTheme.colors.bgRegular), + ) { + PrezelTabs( + tabs = tabs, + pagerState = pagerState, + size = PrezelTabSize.Small, + ) { page -> + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text("Page: $page") + } + } + } + } +} From e883026a41a9ab7ff50d272d202d30f4ecd2e6c5 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Sat, 31 Jan 2026 13:00:56 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20PrezelTabs=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PrezelTabs` 내부의 복잡한 UI 로직을 독립적인 컴포넌트로 분리하여 가독성을 높이고 구조를 개선했습니다. * `PrezelTabsBar`, `PrezelTabItem`, `PrezelTabsPager` 비공개 컴포넌트 추출 * 기존 `PrezelTabs` 내 인라인 구현부를 신규 컴포넌트로 대체 * 코드 스타일 및 들여쓰기 수정 --- .../core/designsystem/component/PrezelTabs.kt | 127 ++++++++++++------ 1 file changed, 85 insertions(+), 42 deletions(-) diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt index 20f8788..3cb9826 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt @@ -57,54 +57,82 @@ fun PrezelTabs( val scope = rememberCoroutineScope() Column(modifier = modifier.fillMaxSize()) { - SecondaryTabRow( - selectedTabIndex = pagerState.currentPage, - modifier = Modifier.fillMaxWidth(), - containerColor = Color.Transparent, - indicator = { - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(2.dp) - .tabIndicatorOffset( - selectedTabIndex = pagerState.currentPage, - ).background(PrezelTheme.colors.solidBlack), - ) + PrezelTabsBar( + tabs = tabs, + pagerState = pagerState, + size = size, + onTabClick = { index, enabled -> + if (!enabled) return@PrezelTabsBar + handleTabClick(scope, pagerState, index) }, - ) { - tabs.forEachIndexed { index, item -> - Tab( - selected = pagerState.currentPage == index, - onClick = { - if (!item.enabled) return@Tab - handleTabClick(scope, pagerState, index) - }, - modifier = Modifier.height(if (size == PrezelTabSize.Small) 36.dp else 48.dp), - enabled = item.enabled, - text = { - PrezelTabLabel( - label = item.label, - size = size, - badgeCount = item.badgeCount, - selected = pagerState.currentPage == index, - ) - }, - selectedContentColor = PrezelTheme.colors.solidBlack, - unselectedContentColor = PrezelTheme.colors.textDisabled, - ) - } - } + ) + + PrezelTabsPager( + pagerState = pagerState, + userScrolledEnabled = userScrolledEnabled, + content = content, + ) + } +} - HorizontalPager( - state = pagerState, - userScrollEnabled = userScrolledEnabled, - overscrollEffect = null, - ) { page -> - content(page) +@Composable +private fun PrezelTabsBar( + tabs: ImmutableList, + pagerState: PagerState, + size: PrezelTabSize, + onTabClick: (index: Int, enabled: Boolean) -> Unit, +) { + SecondaryTabRow( + selectedTabIndex = pagerState.currentPage, + modifier = Modifier.fillMaxWidth(), + containerColor = Color.Transparent, + indicator = { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + .tabIndicatorOffset( + selectedTabIndex = pagerState.currentPage, + ).background(PrezelTheme.colors.solidBlack), + ) + }, + ) { + tabs.forEachIndexed { index, item -> + PrezelTabItem( + item = item, + selected = pagerState.currentPage == index, + size = size, + onClick = { onTabClick(index, item.enabled) }, + ) } } } +@Composable +private fun PrezelTabItem( + item: PrezelTabItem, + selected: Boolean, + size: PrezelTabSize, + onClick: () -> Unit, +) { + Tab( + selected = selected, + onClick = onClick, + modifier = Modifier.height(if (size == PrezelTabSize.Small) 36.dp else 48.dp), + enabled = item.enabled, + text = { + PrezelTabLabel( + label = item.label, + size = size, + badgeCount = item.badgeCount, + selected = selected, + ) + }, + selectedContentColor = PrezelTheme.colors.solidBlack, + unselectedContentColor = PrezelTheme.colors.textDisabled, + ) +} + @Composable private fun PrezelTabLabel( label: String, @@ -132,6 +160,21 @@ private fun PrezelTabLabel( } } +@Composable +private fun PrezelTabsPager( + pagerState: PagerState, + userScrolledEnabled: Boolean, + content: @Composable (pageIndex: Int) -> Unit, +) { + HorizontalPager( + state = pagerState, + userScrollEnabled = userScrolledEnabled, + overscrollEffect = null, + ) { page -> + content(page) + } +} + private fun handleTabClick( scope: CoroutineScope, pagerState: PagerState, From 84244ae4eb2d27f530f8a7401662375bc332b6e7 Mon Sep 17 00:00:00 2001 From: Ham BeomJoon Date: Sat, 31 Jan 2026 13:28:50 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20PrezelTabs=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=B3=80=EC=88=98=EB=AA=85=20=EB=B0=8F=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=ED=95=A8=EC=88=98=EB=AA=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PrezelTabs` 컴포넌트 내의 오타를 수정하고 내부 구성 요소의 명칭을 명확하게 변경했습니다. * `userScrolledEnabled` 파라미터 명칭을 `userScrollEnabled`로 변경 * 내부 컴포넌트 `PrezelTabItem`을 `PrezelTabContent`로 명칭 변경 * 기타 코드 정렬 수정 --- .../prezel/core/designsystem/component/PrezelTabs.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt index 3cb9826..4ed0927 100644 --- a/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt +++ b/Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelTabs.kt @@ -46,7 +46,7 @@ fun PrezelTabs( pagerState: PagerState, modifier: Modifier = Modifier, size: PrezelTabSize = PrezelTabSize.Regular, - userScrolledEnabled: Boolean = true, + userScrollEnabled: Boolean = true, content: @Composable (pageIndex: Int) -> Unit, ) { require(tabs.isNotEmpty()) { "tabs는 비어있을 수 없습니다." } @@ -69,7 +69,7 @@ fun PrezelTabs( PrezelTabsPager( pagerState = pagerState, - userScrolledEnabled = userScrolledEnabled, + userScrollEnabled = userScrollEnabled, content = content, ) } @@ -98,7 +98,7 @@ private fun PrezelTabsBar( }, ) { tabs.forEachIndexed { index, item -> - PrezelTabItem( + PrezelTabContent( item = item, selected = pagerState.currentPage == index, size = size, @@ -109,7 +109,7 @@ private fun PrezelTabsBar( } @Composable -private fun PrezelTabItem( +private fun PrezelTabContent( item: PrezelTabItem, selected: Boolean, size: PrezelTabSize, @@ -163,12 +163,12 @@ private fun PrezelTabLabel( @Composable private fun PrezelTabsPager( pagerState: PagerState, - userScrolledEnabled: Boolean, + userScrollEnabled: Boolean, content: @Composable (pageIndex: Int) -> Unit, ) { HorizontalPager( state = pagerState, - userScrollEnabled = userScrolledEnabled, + userScrollEnabled = userScrollEnabled, overscrollEffect = null, ) { page -> content(page)