From 97e2c5d0f9f241061cde1d73adfa6eaa64b416ef Mon Sep 17 00:00:00 2001 From: StopStone Date: Sat, 1 Nov 2025 00:38:37 +0900 Subject: [PATCH 01/95] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=88=EB=8B=AC=20?= =?UTF-8?q?=EC=B1=99=EA=B8=B8=EC=82=AC=EB=9E=8C=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 홈 화면의 '이달의 리마인더' 섹션에 '전체보기' 버튼을 추가하고, 클릭 시 해당 화면으로 이동하도록 구현했습니다. - `ic_front_8` 벡터 드로어블을 추가하여 '전체보기' 버튼 아이콘으로 사용했습니다. - 전체보기 화면으로 이동하는 `navigateToMonthlyReminderAll` 네비게이션 로직과 `MonthlyReminderAllScreen` 컴포저블을 추가했습니다. --- .../presentation/feature/home/HomeScreen.kt | 69 +++++++++++++++++-- .../feature/home/navigation/HomeNavigation.kt | 5 +- .../presentation/feature/main/NearNavHost.kt | 7 ++ .../MonthlyReminderAllNavigation.kt | 23 +++++++ .../navigation/MonthlyReminderAllScreen.kt | 17 +++++ Near/app/src/main/res/drawable/ic_front_8.xml | 13 ++++ 6 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllScreen.kt create mode 100644 Near/app/src/main/res/drawable/ic_front_8.xml diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 6307c964..f76d753e 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -31,9 +31,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.paint import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource @@ -74,6 +76,7 @@ internal fun HomeRoute( onAlarmClick: () -> Unit = {}, onMyPageClick: () -> Unit = {}, onAddContactClick: () -> Unit = {}, + onMonthlyReminderAllClick: () -> Unit = {}, ) { LaunchedEffect(Unit) { launch { @@ -90,6 +93,7 @@ internal fun HomeRoute( onAlarmClick = onAlarmClick, onMyPageClick = onMyPageClick, onAddContactClick = onAddContactClick, + onMonthlyReminderAllClick = onMonthlyReminderAllClick, contacts = friends.value, monthlyFriends = monthlyFriends.value, memberInfo = memberInfo.value, @@ -104,6 +108,7 @@ internal fun HomeScreen( onMyPageClick: () -> Unit = {}, onAlarmClick: () -> Unit = {}, onAddContactClick: () -> Unit = {}, + onMonthlyReminderAllClick: () -> Unit = {}, memberInfo: MemberInfo?, contacts: List, monthlyFriends: List, @@ -130,7 +135,8 @@ internal fun HomeScreen( R.drawable.img_bg, ), contentScale = ContentScale.FillBounds, - ).fillMaxSize(), + ) + .fillMaxSize(), ) { Spacer(modifier = Modifier.height(statusBarHeightDp)) Row( @@ -174,12 +180,23 @@ internal fun HomeScreen( color = NearTheme.colors.WHITE_FFFFFF, ) Spacer(modifier = Modifier.height(32.dp)) - Text( - text = stringResource(R.string.home_this_month_people), - modifier = Modifier.padding(horizontal = 24.dp), - style = NearTheme.typography.B1_16_BOLD, - color = NearTheme.colors.WHITE_FFFFFF, - ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.home_this_month_people), + modifier = Modifier.padding(horizontal = 24.dp), + style = NearTheme.typography.B1_16_BOLD, + color = NearTheme.colors.WHITE_FFFFFF, + ) + + MonthlyReminderFriendsViewAll( + modifier = Modifier.padding(horizontal = 24.dp), + onMonthlyReminderAllClick = onMonthlyReminderAllClick, + ) + } Spacer(modifier = Modifier.height(16.dp)) if (monthlyFriends.isEmpty()) { Surface( @@ -370,6 +387,36 @@ private fun PagerIndicator(pagerState: PagerState) { } } +@Composable +fun MonthlyReminderFriendsViewAll( + modifier: Modifier = Modifier, + onMonthlyReminderAllClick: () -> Unit = {}, +) { + Row( + modifier = + modifier.onNoRippleClick( + onClick = onMonthlyReminderAllClick, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "전체보기", + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.WHITE_FFFFFF, + modifier = Modifier.alpha(0.8f), + ) + + Spacer(modifier = Modifier.size(6.dp)) + + Image( + painter = painterResource(id = R.drawable.ic_front_8), + colorFilter = ColorFilter.tint(NearTheme.colors.WHITE_FFFFFF), + alpha = 1f, + contentDescription = null, + ) + } +} + @Preview @Composable internal fun HomeScreenPreview() { @@ -408,3 +455,11 @@ internal fun HomeScreenPreview() { ) } } + +@Preview +@Composable +fun MonthlyReminderFriendsViewAllPreview() { + NearTheme { + MonthlyReminderFriendsViewAll() + } +} diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/navigation/HomeNavigation.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/navigation/HomeNavigation.kt index 382ddeb5..61d1b03e 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/navigation/HomeNavigation.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/navigation/HomeNavigation.kt @@ -5,9 +5,6 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import com.alarmy.near.model.Friend -import com.alarmy.near.presentation.feature.friendprofile.FriendProfileViewModel -import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.FRIEND_PROFILE_EDIT_COMPLETE_KEY import com.alarmy.near.presentation.feature.home.HomeRoute import com.alarmy.near.presentation.feature.home.HomeViewModel import kotlinx.serialization.Serializable @@ -30,6 +27,7 @@ fun NavGraphBuilder.homeNavGraph( onAlarmClick: () -> Unit = {}, onMyPageClick: () -> Unit = {}, onAddContactClick: () -> Unit = {}, + onMonthlyReminderAllClick: () -> Unit = {}, ) { composable { backStackEntry -> val viewModel: HomeViewModel = hiltViewModel() @@ -43,6 +41,7 @@ fun NavGraphBuilder.homeNavGraph( onAlarmClick = onAlarmClick, onMyPageClick = onMyPageClick, onAddContactClick = onAddContactClick, + onMonthlyReminderAllClick = onMonthlyReminderAllClick, ) } } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt index e34f35a3..502aa89b 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt @@ -23,6 +23,8 @@ import com.alarmy.near.presentation.feature.home.navigation.homeNavGraph import com.alarmy.near.presentation.feature.home.navigation.navigateToHome import com.alarmy.near.presentation.feature.login.navigation.loginNavGraph import com.alarmy.near.presentation.feature.login.navigation.navigateToLogin +import com.alarmy.near.presentation.feature.mothlyreminderall.navigation.monthlyReminderAllNavGraph +import com.alarmy.near.presentation.feature.mothlyreminderall.navigation.navigateToMonthlyReminderAll import com.alarmy.near.presentation.feature.myprofile.navigation.myProfileNavGraph import com.alarmy.near.presentation.feature.myprofile.navigation.navigateToMyProfile import com.alarmy.near.presentation.feature.myprofile.navigation.navigateToWebView @@ -87,6 +89,11 @@ internal fun NearNavHost( onMyPageClick = { navController.navigateToMyProfile() }, onAlarmClick = {}, onAddContactClick = { navController.navigateToFriendContactCycle() }, + onMonthlyReminderAllClick = { navController.navigateToMonthlyReminderAll() }, + ) + + monthlyReminderAllNavGraph( + onShowErrorSnackBar = onShowSnackbar, ) myProfileNavGraph( diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt new file mode 100644 index 00000000..d9903719 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt @@ -0,0 +1,23 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import kotlinx.serialization.Serializable + +@Serializable +object RouteMonthlyReminderAll + +fun NavController.navigateToMonthlyReminderAll() { + navigate(RouteMonthlyReminderAll) +} + +fun NavGraphBuilder.monthlyReminderAllNavGraph( + onShowErrorSnackBar: (throwable: Throwable?) -> Unit, +) { + composable { backStackEntry -> + MonthlyReminderAllScreen( + onShowErrorSnackBar = onShowErrorSnackBar, + ) + } +} diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllScreen.kt new file mode 100644 index 00000000..8e73cc87 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllScreen.kt @@ -0,0 +1,17 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.navigation + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun MonthlyReminderAllScreen(onShowErrorSnackBar: (throwable: Throwable?) -> Unit) { + Surface( + modifier = Modifier + .fillMaxSize() + .background(Color.Black), + ) { } +} diff --git a/Near/app/src/main/res/drawable/ic_front_8.xml b/Near/app/src/main/res/drawable/ic_front_8.xml new file mode 100644 index 00000000..8c0d485f --- /dev/null +++ b/Near/app/src/main/res/drawable/ic_front_8.xml @@ -0,0 +1,13 @@ + + + From 525fc6bb1853cb4363de744cbe62ab43cf92fc9f Mon Sep 17 00:00:00 2001 From: StopStone Date: Sat, 1 Nov 2025 01:08:09 +0900 Subject: [PATCH 02/95] =?UTF-8?q?feat:=20=EC=9B=94=EB=B3=84=20=EB=A6=AC?= =?UTF-8?q?=EB=A7=88=EC=9D=B8=EB=8D=94=20=EC=B9=9C=EA=B5=AC=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=BB=B4=ED=8F=AC=EC=A0=80=EB=B8=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 월별 전체 리마인더 화면에서 사용될 `MonthlyReminderFriendCard` 컴포저블을 추가했습니다. - 이 카드는 친구의 이름, 리마인더 내용, D-day, 그리고 '챙김 기록하기' 버튼으로 구성됩니다. --- .../components/MonthlyReminderFriendCard.kt | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt new file mode 100644 index 00000000..3adefb4f --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt @@ -0,0 +1,124 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.alarmy.near.R +import com.alarmy.near.presentation.ui.component.button.NearBasicButton +import com.alarmy.near.presentation.ui.extension.dropShadow +import com.alarmy.near.presentation.ui.theme.NearTheme + +@Composable +fun MonthlyReminderFriendCard() { + Card( + modifier = + Modifier + .fillMaxWidth() + .wrapContentHeight() + .dropShadow( + shape = RoundedCornerShape(12.dp), + blur = 16.dp, + spread = 0.dp, + offsetX = 0.dp, + offsetY = 4.dp, + color = Color.Black.copy(0.12f), + ).padding(horizontal = 20.dp), + colors = + CardDefaults.cardColors( + containerColor = NearTheme.colors.WHITE_FFFFFF, + ), + ) { + Spacer(modifier = Modifier.size(20.dp)) + + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + modifier = Modifier.size(32.dp), + painter = painterResource(id = R.drawable.icon_visual_cake), + contentDescription = null, + ) + + Spacer(modifier = Modifier.size(12.dp)) + + Column( + modifier = Modifier.weight(1f), + ) { + Text( + text = "신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구", + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = NearTheme.typography.B2_14_BOLD, + color = NearTheme.colors.BLACK_1A1A1A, + ) + + Spacer(modifier = Modifier.size(6.dp)) + + Text( + text = "생일 축하 전해요", + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.GRAY01_888888, + ) + } + + Spacer(modifier = Modifier.size(10.dp)) + + Text( + modifier = Modifier.align(Alignment.Top), + text = "D-DAY", + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = NearTheme.typography.B2_14_BOLD, + color = NearTheme.colors.BLUE01_5AA2E9, + ) + } + + Spacer(modifier = Modifier.size(16.dp)) + + NearBasicButton( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + onClick = {}, + contentPadding = PaddingValues(12.dp), + ) { + Text( + "챙김 기록하기", + ) + } + Spacer(modifier = Modifier.size(20.dp)) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 700) +@Composable +fun MonthlyReminderFriendCardPreview() { + NearTheme { + MonthlyReminderFriendCard() + } +} From 2daa449dbd1ba372c5891df9944b0efc5498b7ba Mon Sep 17 00:00:00 2001 From: StopStone Date: Sat, 1 Nov 2025 01:15:18 +0900 Subject: [PATCH 03/95] =?UTF-8?q?feat:=20=EC=9B=94=EB=B3=84=20=EB=A6=AC?= =?UTF-8?q?=EB=A7=88=EC=9D=B8=EB=8D=94=20=ED=99=94=EB=A9=B4=20=EB=B9=88=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=BB=B4=ED=8F=AC=EC=A0=80=EB=B8=94=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 월별 리마인더에 표시할 내용이 없을 때 보여줄 `MonthlyReminderEmpty` 컴포저블을 추가했습니다. - 해당 컴포저블은 안내 이미지와 "이번달은 챙길 사람이 없네요."라는 텍스트로 구성됩니다. --- .../components/MonthlyReminderEmpty.kt | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt new file mode 100644 index 00000000..8268823d --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt @@ -0,0 +1,43 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.alarmy.near.R +import com.alarmy.near.presentation.ui.theme.NearTheme + +@Composable +fun MonthlyReminderEmpty() { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(R.drawable.img_100_character_empty), + contentDescription = null, + ) + + Spacer(modifier = Modifier.size(16.dp)) + + Text( + text = "이번달은 챙길 사람이 없네요.", + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.GRAY01_888888, + ) + } +} + +@Preview +@Composable +fun MonthlyReminderEmptyPreview() { + NearTheme { + MonthlyReminderEmpty() + } +} From 1bc2ad35d059f72a11aab603de9d996c9ff91c22 Mon Sep 17 00:00:00 2001 From: StopStone Date: Sat, 1 Nov 2025 01:22:54 +0900 Subject: [PATCH 04/95] =?UTF-8?q?feat:=20=EC=9B=94=EB=B3=84=20=EB=A6=AC?= =?UTF-8?q?=EB=A7=88=EC=9D=B8=EB=8D=94=20=EC=99=84=EB=A3=8C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EC=A0=80=EB=B8=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 월별 리마인더의 완료된 항목을 표시하는 `MonthlyReminderComplete` 컴포저블을 새로 추가했습니다. - 이 컴포저블은 아이콘, 이름, 날짜를 포함하며, 완료된 상태의 UI를 나타냅니다. --- .../components/MonthlyReminderComplete.kt | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt new file mode 100644 index 00000000..4c938770 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt @@ -0,0 +1,58 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.alarmy.near.R +import com.alarmy.near.presentation.ui.theme.NearTheme + +@Composable +fun MonthlyReminderComplete() { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(NearTheme.colors.BG02_F4F9FD) + .padding(vertical = 18.dp, horizontal = 20.dp), + ) { + Image( + painter = painterResource(R.drawable.icon_visual_cake), + contentDescription = null, + ) + Spacer(modifier = Modifier.size(12.dp)) + Text( + text = "신짱구", + style = NearTheme.typography.B2_14_BOLD, + color = NearTheme.colors.BLACK_1A1A1A, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "25.03.20", + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.GRAY01_888888, + ) + } +} + +@Preview +@Composable +fun MonthlyReminderCompletePreview() { + NearTheme { + MonthlyReminderComplete() + } +} From 7dc900c28fade771c60a4e536f32b0bfeca7cfc6 Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 17:32:48 +0900 Subject: [PATCH 05/95] =?UTF-8?q?refactor:=20=EC=9B=94=EB=B3=84=20?= =?UTF-8?q?=EB=A6=AC=EB=A7=88=EC=9D=B8=EB=8D=94=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20ViewModel=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MonthlyReminderAllScreen.kt | 22 +++++++++++++++++++ .../MonthlyReminderAllNavigation.kt | 5 ++--- .../navigation/MonthlyReminderAllScreen.kt | 17 -------------- 3 files changed, 24 insertions(+), 20 deletions(-) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt delete mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllScreen.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt new file mode 100644 index 00000000..85ebe9ba --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -0,0 +1,22 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.hilt.navigation.compose.hiltViewModel + +@Composable +fun MonthlyReminderAllScreen( + viewModel: MonthlyReminderAllViewModel = hiltViewModel(), + onShowErrorSnackBar: (throwable: Throwable?) -> Unit, +) { + Surface( + modifier = + Modifier + .fillMaxSize() + .background(Color.Black), + ) { } +} diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt index d9903719..ea0db6f0 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt @@ -3,6 +3,7 @@ package com.alarmy.near.presentation.feature.mothlyreminderall.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import com.alarmy.near.presentation.feature.mothlyreminderall.MonthlyReminderAllScreen import kotlinx.serialization.Serializable @Serializable @@ -12,9 +13,7 @@ fun NavController.navigateToMonthlyReminderAll() { navigate(RouteMonthlyReminderAll) } -fun NavGraphBuilder.monthlyReminderAllNavGraph( - onShowErrorSnackBar: (throwable: Throwable?) -> Unit, -) { +fun NavGraphBuilder.monthlyReminderAllNavGraph(onShowErrorSnackBar: (throwable: Throwable?) -> Unit) { composable { backStackEntry -> MonthlyReminderAllScreen( onShowErrorSnackBar = onShowErrorSnackBar, diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllScreen.kt deleted file mode 100644 index 8e73cc87..00000000 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllScreen.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.alarmy.near.presentation.feature.mothlyreminderall.navigation - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color - -@Composable -fun MonthlyReminderAllScreen(onShowErrorSnackBar: (throwable: Throwable?) -> Unit) { - Surface( - modifier = Modifier - .fillMaxSize() - .background(Color.Black), - ) { } -} From 19cf75dcc538fb33035820485de09166691d89c7 Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 22:28:06 +0900 Subject: [PATCH 06/95] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=88=EB=8B=AC=20?= =?UTF-8?q?=EC=B1=99=EA=B8=B8=20=EC=82=AC=EB=9E=8C=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UI 상태(로딩, 데이터 있음, 데이터 없음)에 따라 각각 다른 화면을 표시합니다. - 뒤로가기 버튼을 통해 이전 화면으로 이동할 수 있습니다. --- .../presentation/feature/main/NearNavHost.kt | 1 + .../MonthlyReminderAllScreen.kt | 170 +++++++++++++++++- .../MonthlyReminderAllNavigation.kt | 10 +- 3 files changed, 169 insertions(+), 12 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt index 502aa89b..c8012d1f 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/main/NearNavHost.kt @@ -94,6 +94,7 @@ internal fun NearNavHost( monthlyReminderAllNavGraph( onShowErrorSnackBar = onShowSnackbar, + onNavigateBack = { navController.popBackStack() }, ) myProfileNavGraph( diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index 85ebe9ba..32220f5f 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -1,22 +1,174 @@ package com.alarmy.near.presentation.feature.mothlyreminderall import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Surface +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +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.alarmy.near.R +import com.alarmy.near.presentation.feature.mothlyreminderall.components.MonthlyReminderEmpty +import com.alarmy.near.presentation.feature.mothlyreminderall.components.MonthlyReminderFriendCard +import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderUIModel +import com.alarmy.near.presentation.feature.mothlyreminderall.uistate.MonthlyReminderAllUIEvent +import com.alarmy.near.presentation.feature.mothlyreminderall.uistate.MonthlyReminderAllUIState +import com.alarmy.near.presentation.ui.component.NearFrame +import com.alarmy.near.presentation.ui.component.appbar.NearTopAppbar +import com.alarmy.near.presentation.ui.theme.NearTheme +import kotlinx.coroutines.launch @Composable -fun MonthlyReminderAllScreen( +fun MonthlyReminderAllRoute( viewModel: MonthlyReminderAllViewModel = hiltViewModel(), onShowErrorSnackBar: (throwable: Throwable?) -> Unit, + onNavigateBack: () -> Unit = {}, ) { - Surface( - modifier = - Modifier - .fillMaxSize() - .background(Color.Black), - ) { } + val uiState = viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(viewModel.uiEvent) { + launch { + viewModel.uiEvent.collect { event -> + when (event) { + is MonthlyReminderAllUIEvent.NetworkError -> { + onShowErrorSnackBar(IllegalStateException("네트워크 에러가 발생했습니다.")) + } + } + } + } + } + + MonthlyReminderAllScreen( + uiState = uiState.value, + onNavigateBack = onNavigateBack, + ) +} + +@Composable +internal fun MonthlyReminderAllScreen( + modifier: Modifier = Modifier, + uiState: MonthlyReminderAllUIState, + onNavigateBack: () -> Unit = {}, +) { + NearFrame( + modifier = modifier.background(NearTheme.colors.WHITE_FFFFFF), + ) { + NearTopAppbar( + title = "이번달 챙길 사람", + onClickBackButton = onNavigateBack, + ) + + when (uiState) { + is MonthlyReminderAllUIState.Loading -> { + Box( + modifier = + Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator(color = NearTheme.colors.BLUE01_5AA2E9) + } + } + + is MonthlyReminderAllUIState.Success -> { + if (uiState.monthlyReminders.isEmpty()) { + Box( + modifier = + Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + MonthlyReminderEmpty() + } + } else { + LazyColumn( + modifier = + Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(16.dp)) + } + + items( + items = uiState.monthlyReminders, + key = { it.friendId }, + ) { reminder -> + MonthlyReminderFriendCard(reminder = reminder) + } + } + } + } + } + } +} + +@Preview +@Composable +private fun MonthlyReminderAllScreenPreview() { + NearTheme { + MonthlyReminderAllScreen( + uiState = + MonthlyReminderAllUIState.Success( + monthlyReminders = + listOf( + MonthlyReminderUIModel( + friendId = "1", + name = "신짱구", + imageRes = R.drawable.icon_visual_cake, + description = "생일 축하 전해요", + nextContactAt = "2025-11-05", + daysUntilNextContact = "D-4", + ), + MonthlyReminderUIModel( + friendId = "2", + name = "김철수", + imageRes = R.drawable.icon_visual_mail, + description = "가볍게 안부인사 전해요", + nextContactAt = "2025-11-01", + daysUntilNextContact = "D-DAY", + ), + MonthlyReminderUIModel( + friendId = "3", + name = "흰둥이", + imageRes = R.drawable.icon_visual_24_heart, + description = "소중한 날 마음을 전해요", + nextContactAt = "2025-11-10", + daysUntilNextContact = "D-9", + ), + ), + ), + ) + } +} + +@Preview +@Composable +private fun MonthlyReminderAllScreenEmptyPreview() { + NearTheme { + MonthlyReminderAllScreen( + uiState = MonthlyReminderAllUIState.Success(monthlyReminders = emptyList()), + ) + } +} + +@Preview +@Composable +private fun MonthlyReminderAllScreenLoadingPreview() { + NearTheme { + MonthlyReminderAllScreen( + uiState = MonthlyReminderAllUIState.Loading, + ) + } } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt index ea0db6f0..5c2280f7 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/navigation/MonthlyReminderAllNavigation.kt @@ -3,7 +3,7 @@ package com.alarmy.near.presentation.feature.mothlyreminderall.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import com.alarmy.near.presentation.feature.mothlyreminderall.MonthlyReminderAllScreen +import com.alarmy.near.presentation.feature.mothlyreminderall.MonthlyReminderAllRoute import kotlinx.serialization.Serializable @Serializable @@ -13,10 +13,14 @@ fun NavController.navigateToMonthlyReminderAll() { navigate(RouteMonthlyReminderAll) } -fun NavGraphBuilder.monthlyReminderAllNavGraph(onShowErrorSnackBar: (throwable: Throwable?) -> Unit) { +fun NavGraphBuilder.monthlyReminderAllNavGraph( + onShowErrorSnackBar: (throwable: Throwable?) -> Unit, + onNavigateBack: () -> Unit, +) { composable { backStackEntry -> - MonthlyReminderAllScreen( + MonthlyReminderAllRoute( onShowErrorSnackBar = onShowErrorSnackBar, + onNavigateBack = onNavigateBack, ) } } From eb7cf4cd0dcc397406dea3dbac47facaeeb54e61 Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 22:30:40 +0900 Subject: [PATCH 07/95] =?UTF-8?q?feat:=20=EC=9D=B4=EB=8B=AC=EC=9D=98=20?= =?UTF-8?q?=EC=B1=99=EA=B9=80=20=EC=B9=9C=EA=B5=AC=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?UI=20=EB=AA=A8=EB=8D=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/MonthlyReminderFriendCard.kt | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt index 3adefb4f..1b7e2a80 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt @@ -22,12 +22,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.alarmy.near.R +import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderUIModel import com.alarmy.near.presentation.ui.component.button.NearBasicButton import com.alarmy.near.presentation.ui.extension.dropShadow import com.alarmy.near.presentation.ui.theme.NearTheme @Composable -fun MonthlyReminderFriendCard() { +fun MonthlyReminderFriendCard(reminder: MonthlyReminderUIModel) { Card( modifier = Modifier @@ -57,7 +58,7 @@ fun MonthlyReminderFriendCard() { ) { Image( modifier = Modifier.size(32.dp), - painter = painterResource(id = R.drawable.icon_visual_cake), + painter = painterResource(id = reminder.imageRes), contentDescription = null, ) @@ -67,7 +68,7 @@ fun MonthlyReminderFriendCard() { modifier = Modifier.weight(1f), ) { Text( - text = "신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구", + text = reminder.name, overflow = TextOverflow.Ellipsis, maxLines = 1, style = NearTheme.typography.B2_14_BOLD, @@ -77,7 +78,7 @@ fun MonthlyReminderFriendCard() { Spacer(modifier = Modifier.size(6.dp)) Text( - text = "생일 축하 전해요", + text = reminder.description, overflow = TextOverflow.Ellipsis, maxLines = 1, style = NearTheme.typography.B2_14_MEDIUM, @@ -87,13 +88,20 @@ fun MonthlyReminderFriendCard() { Spacer(modifier = Modifier.size(10.dp)) + val (textStyle, textColor) = + if (reminder.daysUntilNextContact == "D-DAY") { + NearTheme.typography.B2_14_BOLD to NearTheme.colors.BLUE01_5AA2E9 + } else { + NearTheme.typography.B2_14_MEDIUM to NearTheme.colors.GRAY01_888888 + } + Text( modifier = Modifier.align(Alignment.Top), - text = "D-DAY", + text = reminder.daysUntilNextContact, overflow = TextOverflow.Ellipsis, maxLines = 1, - style = NearTheme.typography.B2_14_BOLD, - color = NearTheme.colors.BLUE01_5AA2E9, + style = textStyle, + color = textColor, ) } @@ -119,6 +127,16 @@ fun MonthlyReminderFriendCard() { @Composable fun MonthlyReminderFriendCardPreview() { NearTheme { - MonthlyReminderFriendCard() + MonthlyReminderFriendCard( + reminder = + MonthlyReminderUIModel( + friendId = "1", + name = "신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구", + imageRes = R.drawable.icon_visual_cake, + description = "생일 축하 전해요", + nextContactAt = "2025-11-05", + daysUntilNextContact = "D-DAY", + ), + ) } } From d85077a185a690f40cb52a12643ab3c5d57c4ec2 Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 22:39:11 +0900 Subject: [PATCH 08/95] =?UTF-8?q?feat:=20iewModel=20=EB=B0=8F=20UI=20State?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `MonthlyReminderAllViewModel`을 추가하여 월별 리마인더 목록을 가져오고 UI 상태를 관리합니다. - API 응답을 `MonthlyReminderUIModel`로 변환하여 UI에 필요한 데이터를 제공합니다. - `MonthlyReminderAllUIState` sealed interface를 `Loading`, `Empty`, `Success` 상태로 정의하여 UI 상태를 관리하도록 수정했습니다. --- .../MonthlyReminderAllScreen.kt | 51 ++++++------- .../MonthlyReminderAllViewModel.kt | 75 +++++++++++++++++++ .../model/MonthlyReminderTypeInfo.kt | 34 +++++++++ .../model/MonthlyReminderUIModel.kt | 13 ++++ .../uistate/MonthlyReminderAllUIEvent.kt | 6 ++ .../uistate/MonthlyReminderAllUIState.kt | 13 ++++ 6 files changed, 167 insertions(+), 25 deletions(-) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderTypeInfo.kt create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index 32220f5f..8c1f972b 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -80,33 +80,33 @@ internal fun MonthlyReminderAllScreen( } } + is MonthlyReminderAllUIState.Empty -> { + Box( + modifier = + Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + MonthlyReminderEmpty() + } + } + is MonthlyReminderAllUIState.Success -> { - if (uiState.monthlyReminders.isEmpty()) { - Box( - modifier = - Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - MonthlyReminderEmpty() + LazyColumn( + modifier = + Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + Spacer(modifier = Modifier.height(16.dp)) } - } else { - LazyColumn( - modifier = - Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - item { - Spacer(modifier = Modifier.height(16.dp)) - } - items( - items = uiState.monthlyReminders, - key = { it.friendId }, - ) { reminder -> - MonthlyReminderFriendCard(reminder = reminder) - } + items( + items = uiState.monthlyReminders, + key = { it.friendId }, + ) { reminder -> + MonthlyReminderFriendCard(reminder = reminder) } } } @@ -158,7 +158,7 @@ private fun MonthlyReminderAllScreenPreview() { private fun MonthlyReminderAllScreenEmptyPreview() { NearTheme { MonthlyReminderAllScreen( - uiState = MonthlyReminderAllUIState.Success(monthlyReminders = emptyList()), + uiState = MonthlyReminderAllUIState.Empty, ) } } @@ -172,3 +172,4 @@ private fun MonthlyReminderAllScreenLoadingPreview() { ) } } + diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt new file mode 100644 index 00000000..ee5be43b --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -0,0 +1,75 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.alarmy.near.data.repository.FriendRepository +import com.alarmy.near.model.monthly.MonthlyFriend +import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderTypeInfo +import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderUIModel +import com.alarmy.near.presentation.feature.mothlyreminderall.uistate.MonthlyReminderAllUIEvent +import com.alarmy.near.presentation.feature.mothlyreminderall.uistate.MonthlyReminderAllUIState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.time.LocalDate +import javax.inject.Inject + +@HiltViewModel +class MonthlyReminderAllViewModel + @Inject + constructor( + private val friendRepository: FriendRepository, + ) : ViewModel() { + private val _uiEvent = Channel() + val uiEvent = _uiEvent.receiveAsFlow() + + private val _uiState: MutableStateFlow = + MutableStateFlow(MonthlyReminderAllUIState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + fetchMonthlyFriends() + } + + private fun fetchMonthlyFriends() { + friendRepository + .fetchMonthlyFriends() + .onEach { monthlyFriends -> + val uiModels = convertToUIModels(monthlyFriends) + _uiState.update { + if (uiModels.isEmpty()) { + MonthlyReminderAllUIState.Empty + } else { + MonthlyReminderAllUIState.Success(uiModels) + } + } + }.catch { exception -> + viewModelScope.launch { + _uiEvent.send(MonthlyReminderAllUIEvent.NetworkError) + } + }.launchIn(viewModelScope) + } + + private fun convertToUIModels(monthlyFriends: List): List { + val today = LocalDate.now() + return monthlyFriends.map { friend -> + val typeInfo = MonthlyReminderTypeInfo.from(friend.type) + MonthlyReminderUIModel( + friendId = friend.friendId, + name = friend.name, + imageRes = typeInfo.imageRes, + description = typeInfo.description, + nextContactAt = friend.nextContactAt, + daysUntilNextContact = friend.daysUntilNextContact(today), + ) + } + } + } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderTypeInfo.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderTypeInfo.kt new file mode 100644 index 00000000..1714e3c6 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderTypeInfo.kt @@ -0,0 +1,34 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.model + +import androidx.annotation.DrawableRes +import com.alarmy.near.R +import com.alarmy.near.model.monthly.MonthlyFriendType + +enum class MonthlyReminderTypeInfo( + @param:DrawableRes val imageRes: Int, + val description: String, +) { + ANNIVERSARY( + imageRes = R.drawable.icon_visual_24_heart, + description = "소중한 날 마음을 전해요", + ), + BIRTHDAY( + imageRes = R.drawable.icon_visual_cake, + description = "생일 축하 전해요", + ), + MESSAGE( + imageRes = R.drawable.icon_visual_mail, + description = "가볍게 안부인사 전해요", + ), + ; + + companion object { + fun from(type: MonthlyFriendType): MonthlyReminderTypeInfo = + when (type) { + MonthlyFriendType.ANNIVERSARY -> ANNIVERSARY + MonthlyFriendType.BIRTHDAY -> BIRTHDAY + MonthlyFriendType.MESSAGE -> MESSAGE + } + } +} + diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt new file mode 100644 index 00000000..012ec0f5 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt @@ -0,0 +1,13 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.model + +import androidx.annotation.DrawableRes + +data class MonthlyReminderUIModel( + val friendId: String, + val name: String, + @DrawableRes val imageRes: Int, + val description: String, + val nextContactAt: String, + val daysUntilNextContact: String, +) + diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt new file mode 100644 index 00000000..ff93a282 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt @@ -0,0 +1,6 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.uistate + +sealed interface MonthlyReminderAllUIEvent { + data object NetworkError : MonthlyReminderAllUIEvent +} + diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt new file mode 100644 index 00000000..3a16596e --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt @@ -0,0 +1,13 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.uistate + +import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderUIModel + +sealed interface MonthlyReminderAllUIState { + data object Loading : MonthlyReminderAllUIState + + data object Empty : MonthlyReminderAllUIState + + data class Success( + val monthlyReminders: List, + ) : MonthlyReminderAllUIState +} From 7f86285f8a0c10bb8f7ec36aa47f0cbf3fd534ad Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 23:10:47 +0900 Subject: [PATCH 09/95] =?UTF-8?q?feat:=20=EC=9D=B4=EB=8B=AC=EC=9D=98=20?= =?UTF-8?q?=EC=B1=99=EA=B9=80=20=EC=99=84=EB=A3=8C=20=EC=B9=9C=EA=B5=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../near/data/repository/DefaultFriendRepository.kt | 5 +++++ .../com/alarmy/near/data/repository/FriendRepository.kt | 9 +++++++-- .../com/alarmy/near/network/service/FriendService.kt | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/data/repository/DefaultFriendRepository.kt b/Near/app/src/main/java/com/alarmy/near/data/repository/DefaultFriendRepository.kt index e63d0dd0..31045cfa 100644 --- a/Near/app/src/main/java/com/alarmy/near/data/repository/DefaultFriendRepository.kt +++ b/Near/app/src/main/java/com/alarmy/near/data/repository/DefaultFriendRepository.kt @@ -39,6 +39,11 @@ class DefaultFriendRepository ) } + override fun fetchMonthlyCompleteFriends(): Flow> = + apiCallFlow { + friendService.fetchMonthlyCompleteFriends().map { it.toModel() } + } + override fun fetchFriendById(friendId: String): Flow = flow { emit(friendService.fetchFriendById(friendId).toModel()) diff --git a/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt b/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt index c437c4e4..78774f86 100644 --- a/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt +++ b/Near/app/src/main/java/com/alarmy/near/data/repository/FriendRepository.kt @@ -2,9 +2,9 @@ package com.alarmy.near.data.repository import com.alarmy.near.model.Friend import com.alarmy.near.model.FriendRecord -import com.alarmy.near.network.response.FriendInitItemEntity import com.alarmy.near.model.friendsummary.FriendSummary import com.alarmy.near.model.monthly.MonthlyFriend +import com.alarmy.near.network.response.FriendInitItemEntity import com.alarmy.near.presentation.feature.friendcontactcycle.model.FriendContactUIModel import kotlinx.coroutines.flow.Flow @@ -13,6 +13,8 @@ interface FriendRepository { fun fetchMonthlyFriends(): Flow> + fun fetchMonthlyCompleteFriends(): Flow> + fun fetchFriendById(friendId: String): Flow fun updateFriend( @@ -26,5 +28,8 @@ interface FriendRepository { fun recordContact(friendId: String): Flow - fun initFriends(contacts: List, providerType: String): Flow> + fun initFriends( + contacts: List, + providerType: String, + ): Flow> } diff --git a/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt b/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt index c7cf3864..7fe55c5e 100644 --- a/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt +++ b/Near/app/src/main/java/com/alarmy/near/network/service/FriendService.kt @@ -22,6 +22,9 @@ interface FriendService { @GET("/friend/monthly") suspend fun fetchMonthlyFriends(): List + @GET("/friend/monthly/complete") + suspend fun fetchMonthlyCompleteFriends(): List + @GET("/friend/{friendId}") suspend fun fetchFriendById( @Path("friendId") friendId: String, From 1bf63ed528567410e6723b95e22bf2ed8e712050 Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 23:11:06 +0900 Subject: [PATCH 10/95] =?UTF-8?q?chore:=20MonthlyReminderFriendCard=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mothlyreminderall/components/MonthlyReminderFriendCard.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt index 1b7e2a80..d393fc1a 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt @@ -41,7 +41,7 @@ fun MonthlyReminderFriendCard(reminder: MonthlyReminderUIModel) { offsetX = 0.dp, offsetY = 4.dp, color = Color.Black.copy(0.12f), - ).padding(horizontal = 20.dp), + ), colors = CardDefaults.cardColors( containerColor = NearTheme.colors.WHITE_FFFFFF, From 05ac850de2e87bebac7be77b0bcfac55c657b18e Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 23:11:36 +0900 Subject: [PATCH 11/95] =?UTF-8?q?feat:=20=EC=B1=99=EA=B9=80=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EB=AA=A9=EB=A1=9D=20=EC=95=84=EC=9D=B4=ED=85=9C=20?= =?UTF-8?q?UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `MonthlyReminderComplete` 컴포저블이 `MonthlyReminderUIModel` 데이터를 받아 실제 데이터를 표시하도록 수정했습니다. - 날짜 형식을 'yy.MM.dd'로 포맷하는 `formatDate` 함수를 추가했습니다. - 이름이 길어질 경우 말줄임표(...)로 처리되도록 `TextOverflow.Ellipsis` 속성을 적용했습니다. --- .../components/MonthlyReminderComplete.kt | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt index 4c938770..5adf204e 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt @@ -14,13 +14,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.alarmy.near.R +import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderUIModel import com.alarmy.near.presentation.ui.theme.NearTheme +import java.time.LocalDate +import java.time.format.DateTimeFormatter @Composable -fun MonthlyReminderComplete() { +fun MonthlyReminderComplete(reminder: MonthlyReminderUIModel) { + val formattedDate = formatDate(reminder.nextContactAt) + Row( verticalAlignment = Alignment.CenterVertically, modifier = @@ -31,28 +37,49 @@ fun MonthlyReminderComplete() { .padding(vertical = 18.dp, horizontal = 20.dp), ) { Image( - painter = painterResource(R.drawable.icon_visual_cake), + painter = painterResource(reminder.imageRes), contentDescription = null, ) Spacer(modifier = Modifier.size(12.dp)) Text( - text = "신짱구", + text = reminder.name, style = NearTheme.typography.B2_14_BOLD, color = NearTheme.colors.BLACK_1A1A1A, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), ) - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.size(10.dp)) Text( - text = "25.03.20", + text = formattedDate, style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.GRAY01_888888, ) } } +private fun formatDate(dateString: String): String = + try { + val date = LocalDate.parse(dateString, DateTimeFormatter.ofPattern("yyyy-MM-dd")) + date.format(DateTimeFormatter.ofPattern("yy.MM.dd")) + } catch (e: Exception) { + dateString + } + @Preview @Composable fun MonthlyReminderCompletePreview() { NearTheme { - MonthlyReminderComplete() + MonthlyReminderComplete( + reminder = + MonthlyReminderUIModel( + friendId = "1", + name = "신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구", + imageRes = R.drawable.icon_visual_cake, + description = "생일 축하 전해요", + nextContactAt = "2025-03-20", + daysUntilNextContact = "D-9", + ), + ) } } From 5b178df1d93aa49fa69f3f592bb9d4776d59e600 Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 23:12:17 +0900 Subject: [PATCH 12/95] =?UTF-8?q?feat:=20=EC=B1=99=EA=B9=80=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EB=AA=A9=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MonthlyReminderAllScreen.kt | 42 ++++++++++++++++--- .../MonthlyReminderAllViewModel.kt | 41 +++++++++++++++--- .../uistate/MonthlyReminderAllUIState.kt | 1 + 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index 8c1f972b..a5291397 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -6,9 +6,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -18,6 +21,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.alarmy.near.R +import com.alarmy.near.presentation.feature.mothlyreminderall.components.MonthlyReminderComplete import com.alarmy.near.presentation.feature.mothlyreminderall.components.MonthlyReminderEmpty import com.alarmy.near.presentation.feature.mothlyreminderall.components.MonthlyReminderFriendCard import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderUIModel @@ -61,7 +65,10 @@ internal fun MonthlyReminderAllScreen( onNavigateBack: () -> Unit = {}, ) { NearFrame( - modifier = modifier.background(NearTheme.colors.WHITE_FFFFFF), + modifier = + modifier + .background(NearTheme.colors.WHITE_FFFFFF) + .padding(horizontal = 20.dp), ) { NearTopAppbar( title = "이번달 챙길 사람", @@ -104,11 +111,34 @@ internal fun MonthlyReminderAllScreen( items( items = uiState.monthlyReminders, - key = { it.friendId }, + key = { "monthly_${it.friendId}" }, ) { reminder -> MonthlyReminderFriendCard(reminder = reminder) } + + item { + Spacer(modifier = Modifier.size(16.dp)) + } + + if (uiState.completedReminders.isNotEmpty()) { + item { + Text( + text = "챙김 완료", + style = NearTheme.typography.B2_14_BOLD, + color = NearTheme.colors.BLACK_1A1A1A, + ) + } + + items( + items = uiState.completedReminders, + key = { "completed_${it.friendId}" }, + ) { reminder -> + MonthlyReminderComplete(reminder = reminder) + } + } } + + Spacer(modifier = Modifier.size(80.dp)) } } } @@ -139,13 +169,16 @@ private fun MonthlyReminderAllScreenPreview() { nextContactAt = "2025-11-01", daysUntilNextContact = "D-DAY", ), + ), + completedReminders = + listOf( MonthlyReminderUIModel( friendId = "3", name = "흰둥이", imageRes = R.drawable.icon_visual_24_heart, description = "소중한 날 마음을 전해요", - nextContactAt = "2025-11-10", - daysUntilNextContact = "D-9", + nextContactAt = "2025-10-15", + daysUntilNextContact = "D+16", ), ), ), @@ -172,4 +205,3 @@ private fun MonthlyReminderAllScreenLoadingPreview() { ) } } - diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index ee5be43b..a55ba04e 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -35,8 +35,12 @@ class MonthlyReminderAllViewModel MutableStateFlow(MonthlyReminderAllUIState.Loading) val uiState: StateFlow = _uiState.asStateFlow() + private val _monthlyReminders = MutableStateFlow>(emptyList()) + private val _completedReminders = MutableStateFlow>(emptyList()) + init { fetchMonthlyFriends() + fetchMonthlyCompleteFriends() } private fun fetchMonthlyFriends() { @@ -44,13 +48,22 @@ class MonthlyReminderAllViewModel .fetchMonthlyFriends() .onEach { monthlyFriends -> val uiModels = convertToUIModels(monthlyFriends) - _uiState.update { - if (uiModels.isEmpty()) { - MonthlyReminderAllUIState.Empty - } else { - MonthlyReminderAllUIState.Success(uiModels) - } + _monthlyReminders.value = uiModels + updateUIState() + }.catch { exception -> + viewModelScope.launch { + _uiEvent.send(MonthlyReminderAllUIEvent.NetworkError) } + }.launchIn(viewModelScope) + } + + private fun fetchMonthlyCompleteFriends() { + friendRepository + .fetchMonthlyCompleteFriends() + .onEach { monthlyFriends -> + val uiModels = convertToUIModels(monthlyFriends) + _completedReminders.value = uiModels + updateUIState() }.catch { exception -> viewModelScope.launch { _uiEvent.send(MonthlyReminderAllUIEvent.NetworkError) @@ -58,6 +71,22 @@ class MonthlyReminderAllViewModel }.launchIn(viewModelScope) } + private fun updateUIState() { + val monthlyList = _monthlyReminders.value + val completedList = _completedReminders.value + + _uiState.update { + if (monthlyList.isEmpty() && completedList.isEmpty()) { + MonthlyReminderAllUIState.Empty + } else { + MonthlyReminderAllUIState.Success( + monthlyReminders = monthlyList, + completedReminders = completedList, + ) + } + } + } + private fun convertToUIModels(monthlyFriends: List): List { val today = LocalDate.now() return monthlyFriends.map { friend -> diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt index 3a16596e..96acd1d7 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt @@ -9,5 +9,6 @@ sealed interface MonthlyReminderAllUIState { data class Success( val monthlyReminders: List, + val completedReminders: List, ) : MonthlyReminderAllUIState } From 767fe0c6956526703b2aa71233b6a654b52850a4 Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 23:26:34 +0900 Subject: [PATCH 13/95] =?UTF-8?q?feat:=20=EC=97=B0=EB=9D=BD=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=84=B1=EA=B3=B5=20=EB=8B=A4=EC=9D=B4=EC=96=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 연락을 기록했을 때 표시되는 성공 다이얼로그 UI를 구현했습니다. - 다이얼로그는 1.5초 후 자동으로 사라집니다. --- .../MonthlyReminderAllScreen.kt | 31 ++++++++-- .../MonthlyReminderAllViewModel.kt | 16 ++++++ .../components/MonthlyReminderFriendCard.kt | 7 ++- .../components/RecordSuccessDialog.kt | 57 +++++++++++++++++++ .../uistate/MonthlyReminderAllUIEvent.kt | 2 + 5 files changed, 107 insertions(+), 6 deletions(-) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/RecordSuccessDialog.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index a5291397..335a6956 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -14,6 +14,8 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -24,6 +26,7 @@ import com.alarmy.near.R import com.alarmy.near.presentation.feature.mothlyreminderall.components.MonthlyReminderComplete import com.alarmy.near.presentation.feature.mothlyreminderall.components.MonthlyReminderEmpty import com.alarmy.near.presentation.feature.mothlyreminderall.components.MonthlyReminderFriendCard +import com.alarmy.near.presentation.feature.mothlyreminderall.components.RecordSuccessDialog import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderUIModel import com.alarmy.near.presentation.feature.mothlyreminderall.uistate.MonthlyReminderAllUIEvent import com.alarmy.near.presentation.feature.mothlyreminderall.uistate.MonthlyReminderAllUIState @@ -39,6 +42,7 @@ fun MonthlyReminderAllRoute( onNavigateBack: () -> Unit = {}, ) { val uiState = viewModel.uiState.collectAsStateWithLifecycle() + val recordSuccessDialogState = remember { mutableStateOf(false) } LaunchedEffect(viewModel.uiEvent) { launch { @@ -47,6 +51,10 @@ fun MonthlyReminderAllRoute( is MonthlyReminderAllUIEvent.NetworkError -> { onShowErrorSnackBar(IllegalStateException("네트워크 에러가 발생했습니다.")) } + + is MonthlyReminderAllUIEvent.RecordFriendShipSuccess -> { + recordSuccessDialogState.value = true + } } } } @@ -54,7 +62,12 @@ fun MonthlyReminderAllRoute( MonthlyReminderAllScreen( uiState = uiState.value, + recordSuccessDialogState = recordSuccessDialogState.value, onNavigateBack = onNavigateBack, + onRecordFriendShip = viewModel::onRecordFriendShip, + onDismissRecordSuccessDialog = { + recordSuccessDialogState.value = false + }, ) } @@ -62,13 +75,19 @@ fun MonthlyReminderAllRoute( internal fun MonthlyReminderAllScreen( modifier: Modifier = Modifier, uiState: MonthlyReminderAllUIState, + recordSuccessDialogState: Boolean = false, onNavigateBack: () -> Unit = {}, + onRecordFriendShip: (String) -> Unit = {}, + onDismissRecordSuccessDialog: () -> Unit = {}, ) { + if (recordSuccessDialogState) { + RecordSuccessDialog(onDismiss = onDismissRecordSuccessDialog) + } + NearFrame( modifier = modifier - .background(NearTheme.colors.WHITE_FFFFFF) - .padding(horizontal = 20.dp), + .background(NearTheme.colors.WHITE_FFFFFF), ) { NearTopAppbar( title = "이번달 챙길 사람", @@ -102,7 +121,8 @@ internal fun MonthlyReminderAllScreen( LazyColumn( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .padding(horizontal = 20.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { item { @@ -113,7 +133,10 @@ internal fun MonthlyReminderAllScreen( items = uiState.monthlyReminders, key = { "monthly_${it.friendId}" }, ) { reminder -> - MonthlyReminderFriendCard(reminder = reminder) + MonthlyReminderFriendCard( + reminder = reminder, + onRecordClick = onRecordFriendShip, + ) } item { diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index a55ba04e..d022c526 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -87,6 +87,22 @@ class MonthlyReminderAllViewModel } } + fun onRecordFriendShip(friendId: String) { + friendRepository + .recordContact(friendId) + .onEach { _ -> + viewModelScope.launch { + _uiEvent.send(MonthlyReminderAllUIEvent.RecordFriendShipSuccess) + } + fetchMonthlyFriends() + fetchMonthlyCompleteFriends() + }.catch { exception -> + viewModelScope.launch { + _uiEvent.send(MonthlyReminderAllUIEvent.NetworkError) + } + }.launchIn(viewModelScope) + } + private fun convertToUIModels(monthlyFriends: List): List { val today = LocalDate.now() return monthlyFriends.map { friend -> diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt index d393fc1a..ab46936f 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt @@ -28,7 +28,10 @@ import com.alarmy.near.presentation.ui.extension.dropShadow import com.alarmy.near.presentation.ui.theme.NearTheme @Composable -fun MonthlyReminderFriendCard(reminder: MonthlyReminderUIModel) { +fun MonthlyReminderFriendCard( + reminder: MonthlyReminderUIModel, + onRecordClick: (String) -> Unit = {}, +) { Card( modifier = Modifier @@ -112,7 +115,7 @@ fun MonthlyReminderFriendCard(reminder: MonthlyReminderUIModel) { Modifier .fillMaxWidth() .padding(horizontal = 20.dp), - onClick = {}, + onClick = { onRecordClick(reminder.friendId) }, contentPadding = PaddingValues(12.dp), ) { Text( diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/RecordSuccessDialog.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/RecordSuccessDialog.kt new file mode 100644 index 00000000..fedd5db9 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/RecordSuccessDialog.kt @@ -0,0 +1,57 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.alarmy.near.R +import com.alarmy.near.presentation.ui.theme.NearTheme +import kotlinx.coroutines.delay + +@Composable +fun RecordSuccessDialog(onDismiss: () -> Unit) { + LaunchedEffect(Unit) { + delay(1500) + onDismiss() + } + + Dialog(onDismissRequest = onDismiss) { + Column( + modifier = + Modifier + .width(255.dp) + .height(186.dp) + .background( + color = NearTheme.colors.WHITE_FFFFFF, + shape = RoundedCornerShape(16.dp), + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(R.drawable.img_100_character_success), + contentDescription = null, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + stringResource(R.string.friend_profile_info_contact_success_text), + style = NearTheme.typography.B1_16_BOLD, + color = Color(0xff222222), + ) + } + } +} diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt index ff93a282..494e6a56 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt @@ -2,5 +2,7 @@ package com.alarmy.near.presentation.feature.mothlyreminderall.uistate sealed interface MonthlyReminderAllUIEvent { data object NetworkError : MonthlyReminderAllUIEvent + + data object RecordFriendShipSuccess : MonthlyReminderAllUIEvent } From 761cdaf53698cc802af56766b119d920de77309e Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 23:37:58 +0900 Subject: [PATCH 14/95] =?UTF-8?q?refactor:=20=EC=B1=99=EA=B9=80=20?= =?UTF-8?q?=EC=B9=9C=EA=B5=AC=20=EC=99=84=EB=A3=8C=20=EC=8B=9C=20UI=20?= =?UTF-8?q?=EC=A6=89=EC=8B=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 월별 리마인더 목록에서 완료 처리한 친구를 연락 완료 목록으로 즉시 이동시켜 UI에 반영하도록 수정했습니다. - 기존의 네트워크 통신을 통한 목록 갱신 방식 대신, 로컬에서 목록을 직접 업데이트하여 불필요한 API 호출을 제거하고 응답성을 개선했습니다. --- .../MonthlyReminderAllViewModel.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index d022c526..26b0242b 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -74,13 +74,15 @@ class MonthlyReminderAllViewModel private fun updateUIState() { val monthlyList = _monthlyReminders.value val completedList = _completedReminders.value + val completedFriendIds = completedList.map { it.friendId }.toSet() + val filteredMonthlyList = monthlyList.filter { it.friendId !in completedFriendIds } _uiState.update { - if (monthlyList.isEmpty() && completedList.isEmpty()) { + if (filteredMonthlyList.isEmpty() && completedList.isEmpty()) { MonthlyReminderAllUIState.Empty } else { MonthlyReminderAllUIState.Success( - monthlyReminders = monthlyList, + monthlyReminders = filteredMonthlyList, completedReminders = completedList, ) } @@ -94,8 +96,12 @@ class MonthlyReminderAllViewModel viewModelScope.launch { _uiEvent.send(MonthlyReminderAllUIEvent.RecordFriendShipSuccess) } - fetchMonthlyFriends() - fetchMonthlyCompleteFriends() + val recordedFriend = _monthlyReminders.value.find { it.friendId == friendId } + if (recordedFriend != null) { + _monthlyReminders.value = _monthlyReminders.value.filter { it.friendId != friendId } + _completedReminders.value = listOf(recordedFriend) + _completedReminders.value + updateUIState() + } }.catch { exception -> viewModelScope.launch { _uiEvent.send(MonthlyReminderAllUIEvent.NetworkError) From 7a7334d4c6e5b46ee101267fec30c00c2d388a22 Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 23:46:12 +0900 Subject: [PATCH 15/95] =?UTF-8?q?refactor:=20=EC=9B=94=EB=B3=84=20?= =?UTF-8?q?=EB=A6=AC=EB=A7=88=EC=9D=B8=EB=8D=94=20=EC=97=86=EC=9D=8C=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mothlyreminderall/MonthlyReminderAllScreen.kt | 9 +-------- .../components/MonthlyReminderEmpty.kt | 11 ++++++++++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index 335a6956..f03ca889 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -107,14 +107,7 @@ internal fun MonthlyReminderAllScreen( } is MonthlyReminderAllUIState.Empty -> { - Box( - modifier = - Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - MonthlyReminderEmpty() - } + MonthlyReminderEmpty() } is MonthlyReminderAllUIState.Success -> { diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt index 8268823d..bd9a2ad5 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt @@ -1,8 +1,11 @@ package com.alarmy.near.presentation.feature.mothlyreminderall.components import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -17,8 +20,14 @@ import com.alarmy.near.presentation.ui.theme.NearTheme @Composable fun MonthlyReminderEmpty() { Column( + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, ) { + Spacer(modifier = Modifier.fillMaxHeight(0.28f)) + Image( painter = painterResource(R.drawable.img_100_character_empty), contentDescription = null, @@ -34,7 +43,7 @@ fun MonthlyReminderEmpty() { } } -@Preview +@Preview(showBackground = true) @Composable fun MonthlyReminderEmptyPreview() { NearTheme { From 39c7efc9ba675ac8b4253a1abfe9651623dc2ae5 Mon Sep 17 00:00:00 2001 From: stopstone Date: Sat, 1 Nov 2025 23:55:13 +0900 Subject: [PATCH 16/95] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=B2=88=EB=8B=AC?= =?UTF-8?q?=20=EC=B1=99=EA=B8=B8=20=EC=82=AC=EB=9E=8C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MonthlyReminderAllScreen.kt | 19 ++++++++++----- .../MonthlyReminderAllViewModel.kt | 24 +++++++++++-------- .../components/MonthlyReminderComplete.kt | 2 +- .../components/MonthlyReminderEmpty.kt | 3 ++- .../components/MonthlyReminderFriendCard.kt | 21 ++++++++-------- .../model/MonthlyReminderTypeInfo.kt | 9 +++---- .../model/MonthlyReminderUIModel.kt | 3 ++- Near/app/src/main/res/values/strings.xml | 10 ++++++++ 8 files changed, 58 insertions(+), 33 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index f03ca889..5032b30d 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -18,6 +18,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -41,6 +43,7 @@ fun MonthlyReminderAllRoute( onShowErrorSnackBar: (throwable: Throwable?) -> Unit, onNavigateBack: () -> Unit = {}, ) { + val context = LocalContext.current val uiState = viewModel.uiState.collectAsStateWithLifecycle() val recordSuccessDialogState = remember { mutableStateOf(false) } @@ -49,7 +52,11 @@ fun MonthlyReminderAllRoute( viewModel.uiEvent.collect { event -> when (event) { is MonthlyReminderAllUIEvent.NetworkError -> { - onShowErrorSnackBar(IllegalStateException("네트워크 에러가 발생했습니다.")) + onShowErrorSnackBar( + IllegalStateException( + context.getString(R.string.monthly_reminder_all_network_error) + ) + ) } is MonthlyReminderAllUIEvent.RecordFriendShipSuccess -> { @@ -90,7 +97,7 @@ internal fun MonthlyReminderAllScreen( .background(NearTheme.colors.WHITE_FFFFFF), ) { NearTopAppbar( - title = "이번달 챙길 사람", + title = stringResource(R.string.monthly_reminder_all_title), onClickBackButton = onNavigateBack, ) @@ -139,7 +146,7 @@ internal fun MonthlyReminderAllScreen( if (uiState.completedReminders.isNotEmpty()) { item { Text( - text = "챙김 완료", + text = stringResource(R.string.monthly_reminder_all_completed_section_title), style = NearTheme.typography.B2_14_BOLD, color = NearTheme.colors.BLACK_1A1A1A, ) @@ -173,7 +180,7 @@ private fun MonthlyReminderAllScreenPreview() { friendId = "1", name = "신짱구", imageRes = R.drawable.icon_visual_cake, - description = "생일 축하 전해요", + descriptionRes = R.string.monthly_reminder_all_type_birthday_description, nextContactAt = "2025-11-05", daysUntilNextContact = "D-4", ), @@ -181,7 +188,7 @@ private fun MonthlyReminderAllScreenPreview() { friendId = "2", name = "김철수", imageRes = R.drawable.icon_visual_mail, - description = "가볍게 안부인사 전해요", + descriptionRes = R.string.monthly_reminder_all_type_message_description, nextContactAt = "2025-11-01", daysUntilNextContact = "D-DAY", ), @@ -192,7 +199,7 @@ private fun MonthlyReminderAllScreenPreview() { friendId = "3", name = "흰둥이", imageRes = R.drawable.icon_visual_24_heart, - description = "소중한 날 마음을 전해요", + descriptionRes = R.string.monthly_reminder_all_type_anniversary_description, nextContactAt = "2025-10-15", daysUntilNextContact = "D+16", ), diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index 26b0242b..8017442c 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -111,16 +111,20 @@ class MonthlyReminderAllViewModel private fun convertToUIModels(monthlyFriends: List): List { val today = LocalDate.now() - return monthlyFriends.map { friend -> - val typeInfo = MonthlyReminderTypeInfo.from(friend.type) - MonthlyReminderUIModel( - friendId = friend.friendId, - name = friend.name, - imageRes = typeInfo.imageRes, - description = typeInfo.description, - nextContactAt = friend.nextContactAt, - daysUntilNextContact = friend.daysUntilNextContact(today), - ) + return monthlyFriends.mapNotNull { friend -> + try { + val typeInfo = MonthlyReminderTypeInfo.from(friend.type) + MonthlyReminderUIModel( + friendId = friend.friendId, + name = friend.name, + imageRes = typeInfo.imageRes, + descriptionRes = typeInfo.descriptionRes, + nextContactAt = friend.nextContactAt, + daysUntilNextContact = friend.daysUntilNextContact(today), + ) + } catch (e: Exception) { + null + } } } } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt index 5adf204e..8b610039 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt @@ -76,7 +76,7 @@ fun MonthlyReminderCompletePreview() { friendId = "1", name = "신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구", imageRes = R.drawable.icon_visual_cake, - description = "생일 축하 전해요", + descriptionRes = R.string.monthly_reminder_all_type_birthday_description, nextContactAt = "2025-03-20", daysUntilNextContact = "D-9", ), diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt index bd9a2ad5..3e9ac6e2 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderEmpty.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.alarmy.near.R @@ -36,7 +37,7 @@ fun MonthlyReminderEmpty() { Spacer(modifier = Modifier.size(16.dp)) Text( - text = "이번달은 챙길 사람이 없네요.", + text = stringResource(R.string.monthly_reminder_all_empty_text), style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.GRAY01_888888, ) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt index ab46936f..526bcafc 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -78,15 +79,15 @@ fun MonthlyReminderFriendCard( color = NearTheme.colors.BLACK_1A1A1A, ) - Spacer(modifier = Modifier.size(6.dp)) + Spacer(modifier = Modifier.size(6.dp)) - Text( - text = reminder.description, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.GRAY01_888888, - ) + Text( + text = stringResource(reminder.descriptionRes), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.GRAY01_888888, + ) } Spacer(modifier = Modifier.size(10.dp)) @@ -119,7 +120,7 @@ fun MonthlyReminderFriendCard( contentPadding = PaddingValues(12.dp), ) { Text( - "챙김 기록하기", + text = stringResource(R.string.monthly_reminder_all_record_button_text), ) } Spacer(modifier = Modifier.size(20.dp)) @@ -136,7 +137,7 @@ fun MonthlyReminderFriendCardPreview() { friendId = "1", name = "신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구신짱구", imageRes = R.drawable.icon_visual_cake, - description = "생일 축하 전해요", + descriptionRes = R.string.monthly_reminder_all_type_birthday_description, nextContactAt = "2025-11-05", daysUntilNextContact = "D-DAY", ), diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderTypeInfo.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderTypeInfo.kt index 1714e3c6..87778504 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderTypeInfo.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderTypeInfo.kt @@ -1,24 +1,25 @@ package com.alarmy.near.presentation.feature.mothlyreminderall.model import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import com.alarmy.near.R import com.alarmy.near.model.monthly.MonthlyFriendType enum class MonthlyReminderTypeInfo( @param:DrawableRes val imageRes: Int, - val description: String, + @param:StringRes val descriptionRes: Int, ) { ANNIVERSARY( imageRes = R.drawable.icon_visual_24_heart, - description = "소중한 날 마음을 전해요", + descriptionRes = R.string.monthly_reminder_all_type_anniversary_description, ), BIRTHDAY( imageRes = R.drawable.icon_visual_cake, - description = "생일 축하 전해요", + descriptionRes = R.string.monthly_reminder_all_type_birthday_description, ), MESSAGE( imageRes = R.drawable.icon_visual_mail, - description = "가볍게 안부인사 전해요", + descriptionRes = R.string.monthly_reminder_all_type_message_description, ), ; diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt index 012ec0f5..1a8b4ad7 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt @@ -1,12 +1,13 @@ package com.alarmy.near.presentation.feature.mothlyreminderall.model import androidx.annotation.DrawableRes +import androidx.annotation.StringRes data class MonthlyReminderUIModel( val friendId: String, val name: String, @DrawableRes val imageRes: Int, - val description: String, + @StringRes val descriptionRes: Int, val nextContactAt: String, val daysUntilNextContact: String, ) diff --git a/Near/app/src/main/res/values/strings.xml b/Near/app/src/main/res/values/strings.xml index b926446c..0022b7e5 100644 --- a/Near/app/src/main/res/values/strings.xml +++ b/Near/app/src/main/res/values/strings.xml @@ -85,6 +85,16 @@ 날짜 메모 + + 이번달 챙길 사람 + 챙김 완료 + 이번달은 챙길 사람이 없네요. + 챙김 기록하기 + 네트워크 에러가 발생했습니다. + 소중한 날 마음을 전해요 + 생일 축하 전해요 + 가볍게 안부인사 전해요 + 매일 매주 From b7146adcca0b73151c911bf2a92e1fdc881aeeb7 Mon Sep 17 00:00:00 2001 From: stopstone Date: Mon, 3 Nov 2025 16:28:09 +0900 Subject: [PATCH 17/95] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=B2=88=EB=8B=AC?= =?UTF-8?q?=20=EC=B1=99=EA=B8=B8=20=EC=82=AC=EB=9E=8C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EC=B1=99=EA=B9=80=20=EC=99=84=EB=A3=8C=20=EC=83=81=EB=8B=A8?= =?UTF-8?q?=20=ED=8C=A8=EB=94=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 불필요한 Spacer를 제거하여 레이아웃을 개선했습니다. - LazyColumn의 컨텐츠 패딩을 적용하여 상하단 여백을 조정했습니다. --- .../mothlyreminderall/MonthlyReminderAllScreen.kt | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index 5032b30d..64ef7df2 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -54,8 +53,8 @@ fun MonthlyReminderAllRoute( is MonthlyReminderAllUIEvent.NetworkError -> { onShowErrorSnackBar( IllegalStateException( - context.getString(R.string.monthly_reminder_all_network_error) - ) + context.getString(R.string.monthly_reminder_all_network_error), + ), ) } @@ -125,10 +124,6 @@ internal fun MonthlyReminderAllScreen( .padding(horizontal = 20.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - item { - Spacer(modifier = Modifier.height(16.dp)) - } - items( items = uiState.monthlyReminders, key = { "monthly_${it.friendId}" }, @@ -139,12 +134,10 @@ internal fun MonthlyReminderAllScreen( ) } - item { - Spacer(modifier = Modifier.size(16.dp)) - } - if (uiState.completedReminders.isNotEmpty()) { item { + Spacer(modifier = Modifier.size(16.dp)) + Text( text = stringResource(R.string.monthly_reminder_all_completed_section_title), style = NearTheme.typography.B2_14_BOLD, From 6e8b124201683e3b9a536fab80cac94349be3a4b Mon Sep 17 00:00:00 2001 From: stopstone Date: Mon, 3 Nov 2025 16:32:11 +0900 Subject: [PATCH 18/95] =?UTF-8?q?refactor:=20=EC=97=B0=EB=9D=BD=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20uiState=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MonthlyReminderAllScreen.kt | 3 ++- .../MonthlyReminderAllViewModel.kt | 19 ++++++++++--------- .../uistate/MonthlyReminderAllUIState.kt | 1 + 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index 64ef7df2..b957f845 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -134,7 +134,7 @@ internal fun MonthlyReminderAllScreen( ) } - if (uiState.completedReminders.isNotEmpty()) { + if (uiState.hasCompletedReminders) { item { Spacer(modifier = Modifier.size(16.dp)) @@ -197,6 +197,7 @@ private fun MonthlyReminderAllScreenPreview() { daysUntilNextContact = "D+16", ), ), + hasCompletedReminders = true, ), ) } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index 8017442c..4a260145 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -77,17 +77,18 @@ class MonthlyReminderAllViewModel val completedFriendIds = completedList.map { it.friendId }.toSet() val filteredMonthlyList = monthlyList.filter { it.friendId !in completedFriendIds } - _uiState.update { - if (filteredMonthlyList.isEmpty() && completedList.isEmpty()) { - MonthlyReminderAllUIState.Empty - } else { - MonthlyReminderAllUIState.Success( - monthlyReminders = filteredMonthlyList, - completedReminders = completedList, - ) - } + _uiState.update { + if (filteredMonthlyList.isEmpty() && completedList.isEmpty()) { + MonthlyReminderAllUIState.Empty + } else { + MonthlyReminderAllUIState.Success( + monthlyReminders = filteredMonthlyList, + completedReminders = completedList, + hasCompletedReminders = completedList.isNotEmpty(), + ) } } + } fun onRecordFriendShip(friendId: String) { friendRepository diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt index 96acd1d7..e6151f63 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIState.kt @@ -10,5 +10,6 @@ sealed interface MonthlyReminderAllUIState { data class Success( val monthlyReminders: List, val completedReminders: List, + val hasCompletedReminders: Boolean, ) : MonthlyReminderAllUIState } From 02ef8577c10447c35c1c02b52144061b93ef6b0e Mon Sep 17 00:00:00 2001 From: stopstone Date: Tue, 4 Nov 2025 16:45:15 +0900 Subject: [PATCH 19/95] =?UTF-8?q?refactor:=20=EC=97=B0=EB=9D=BD=EC=B2=98?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=BC=EC=9E=90=20=ED=9A=A8=EA=B3=BC=20=EC=88=98?= =?UTF-8?q?=EC=B9=98=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/NearListModuleBackground.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/NearListModuleBackground.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/NearListModuleBackground.kt index 151d6f8c..1e698cca 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/NearListModuleBackground.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/NearListModuleBackground.kt @@ -12,12 +12,13 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.alarmy.near.R +import com.alarmy.near.presentation.ui.extension.dropShadow import com.alarmy.near.presentation.ui.extension.onNoRippleClick import com.alarmy.near.presentation.ui.theme.NearTheme @@ -38,8 +39,12 @@ fun NearListModuleBackground( modifier = modifier .fillMaxWidth() - .shadow( - elevation = 4.dp, + .dropShadow( + blur = 16.dp, + offsetX = 0.dp, + offsetY = 4.dp, + spread = 0.dp, + color = Color.Black.copy(alpha = 0.12f), shape = RoundedCornerShape(12.dp), ).background( color = NearTheme.colors.WHITE_FFFFFF, From 4479c4959729b147613db33384884b550df739c9 Mon Sep 17 00:00:00 2001 From: stopstone Date: Tue, 4 Nov 2025 16:52:13 +0900 Subject: [PATCH 20/95] =?UTF-8?q?refactor:=20CycleSettingBottomSheet=20?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0=EC=84=A4=EC=A0=95=20=EA=B3=B5=EB=B0=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/CycleSettingBottomSheet.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt index 826334a4..01a66cce 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt @@ -46,8 +46,8 @@ fun CycleSettingBottomSheet( modifier: Modifier = Modifier, ) { // 선택된 주기 상태 관리 (기존 선택값이 있으면 그것을 사용, 없으면 매주를 기본값으로) - var selectedInterval by remember(isVisible) { - mutableStateOf(currentSelectedInterval ?: ReminderInterval.EVERY_WEEK) + var selectedInterval by remember(isVisible) { + mutableStateOf(currentSelectedInterval ?: ReminderInterval.EVERY_WEEK) } if (isVisible) { @@ -102,7 +102,7 @@ fun CycleSettingBottomSheet( fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, ), ) { - append(stringResource(R.string.friend_contact_cycle_weekly_prefix)) + append(stringResource(R.string.friend_contact_cycle_weekly_prefix) + " ") } withStyle( style = @@ -136,7 +136,7 @@ fun CycleSettingBottomSheet( Text( text = "다음 주기 : ${ - selectedInterval?.let { DateExtension.getNextCycleDate(it) } + selectedInterval?.let { DateExtension.getNextCycleDate(it) } ?: DateExtension.getNextWeekSameDay() }", style = NearTheme.typography.B2_14_MEDIUM, @@ -187,7 +187,7 @@ fun CycleSettingBottomSheet( ContactCycleButtons( onLeftButtonClick = onDismiss, - onRightButtonClick = { + onRightButtonClick = { selectedInterval?.let { interval -> onComplete(interval) onDismiss() From 119e70cdb417e7e9ddfe4a54f2b3f09668003805 Mon Sep 17 00:00:00 2001 From: stopstone Date: Tue, 4 Nov 2025 17:14:30 +0900 Subject: [PATCH 21/95] =?UTF-8?q?feat:=20=EC=A3=BC=EA=B8=B0=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=94=ED=85=80=20=EC=8B=9C=ED=8A=B8=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=8F=99=EC=A0=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존에 '매주'로 고정되어 있던 주기 설정 바텀 시트의 텍스트를 `selectedInterval` 값에 따라 '매주', '2주마다', '매달'로 동적으로 변경되도록 수정했습니다. - `DateExtension.getCycleText()`를 사용하여 선택된 주기에 맞는 텍스트를 가져오도록 구현했습니다. - 주기 텍스트에서 요일 부분에만 다른 스타일이 적용되도록 로직을 개선했습니다. --- .../components/CycleSettingBottomSheet.kt | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt index 01a66cce..5ca1ee81 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt @@ -95,23 +95,39 @@ fun CycleSettingBottomSheet( Text( text = buildAnnotatedString { - withStyle( - style = - SpanStyle( - color = NearTheme.colors.BLACK_1A1A1A, - fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, - ), - ) { - append(stringResource(R.string.friend_contact_cycle_weekly_prefix) + " ") - } - withStyle( - style = - SpanStyle( - color = NearTheme.colors.BLUE01_5AA2E9, - fontWeight = NearTheme.typography.B2_14_BOLD.fontWeight, - ), - ) { - append(DateExtension.getTodayDayOfWeekInKorean()) + val interval = selectedInterval ?: ReminderInterval.EVERY_WEEK + val cycleText = DateExtension.getCycleText(interval) + val firstSpaceIndex = cycleText.indexOf(' ') + + if (firstSpaceIndex != -1) { + withStyle( + style = + SpanStyle( + color = NearTheme.colors.BLACK_1A1A1A, + fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, + ), + ) { + append(cycleText.substring(0, firstSpaceIndex + 1)) + } + withStyle( + style = + SpanStyle( + color = NearTheme.colors.BLUE01_5AA2E9, + fontWeight = NearTheme.typography.B2_14_BOLD.fontWeight, + ), + ) { + append(cycleText.substring(firstSpaceIndex + 1)) + } + } else { + withStyle( + style = + SpanStyle( + color = NearTheme.colors.BLACK_1A1A1A, + fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, + ), + ) { + append(cycleText) + } } }, style = NearTheme.typography.B2_14_MEDIUM, From 00e427cd78925a8f953637c3e2c172dc267a99ab Mon Sep 17 00:00:00 2001 From: stopstone Date: Tue, 4 Nov 2025 17:16:35 +0900 Subject: [PATCH 22/95] =?UTF-8?q?refactor:=20=EC=97=B0=EB=9D=BD=20?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0=20=EC=84=A4=EC=A0=95=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?'=EB=8B=A4=EC=9D=8C=20=EC=A3=BC=EA=B8=B0'=20=ED=85=8D=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20strings.xml=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/CycleSettingBottomSheet.kt | 10 ++++++---- Near/app/src/main/res/values/strings.xml | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt index 5ca1ee81..d09b78a6 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt @@ -151,10 +151,12 @@ fun CycleSettingBottomSheet( Spacer(modifier = Modifier.size(20.dp)) Text( - text = "다음 주기 : ${ - selectedInterval?.let { DateExtension.getNextCycleDate(it) } - ?: DateExtension.getNextWeekSameDay() - }", + text = + stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + + ( + selectedInterval?.let { DateExtension.getNextCycleDate(it) } + ?: DateExtension.getNextWeekSameDay() + ), style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.GRAY01_888888, ) diff --git a/Near/app/src/main/res/values/strings.xml b/Near/app/src/main/res/values/strings.xml index b926446c..97d54c0e 100644 --- a/Near/app/src/main/res/values/strings.xml +++ b/Near/app/src/main/res/values/strings.xml @@ -179,6 +179,7 @@ 주기 설정 매주 %1$s 매주 + 다음 주기 : 취소 완료 나중에 하기 From 3160c32280f2378b52b49b82a2e944d128f00022 Mon Sep 17 00:00:00 2001 From: stopstone Date: Tue, 4 Nov 2025 17:21:30 +0900 Subject: [PATCH 23/95] =?UTF-8?q?refactor:=20=EC=97=B0=EB=9D=BD=20?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0=20=ED=99=94=EB=A9=B4=20UI=20=ED=8C=A8?= =?UTF-8?q?=EB=94=A9=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ContactCycleContent.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactCycleContent.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactCycleContent.kt index ae5c5116..3a4b35b3 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactCycleContent.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactCycleContent.kt @@ -64,10 +64,10 @@ fun ColumnScope.ContactCycleContent( ) } - Spacer(modifier = Modifier.size(14.dp)) - // 한번에 설정이 활성화되었을 때만 표시 if (isBulkSettingEnabled) { + Spacer(modifier = Modifier.size(12.dp)) + Row( modifier = Modifier @@ -85,7 +85,9 @@ fun ColumnScope.ContactCycleContent( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = selectedCycle?.let { DateExtension.getCycleText(it) } ?: stringResource(R.string.friend_contact_cycle_weekly_format, DateExtension.getTodayDayOfWeekInKorean()), + text = + selectedCycle?.let { DateExtension.getCycleText(it) } + ?: stringResource(R.string.friend_contact_cycle_weekly_format, DateExtension.getTodayDayOfWeekInKorean()), style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.BLACK_1A1A1A, ) @@ -101,7 +103,7 @@ fun ColumnScope.ContactCycleContent( // 리스트가 있을 때만 밑에 리스트 표시 if (contacts.isNotEmpty()) { - Spacer(modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.size(24.dp)) LazyColumn( modifier = Modifier.weight(1f), From 5d6b6ddd934f91a8421a9721ee21d271083071b2 Mon Sep 17 00:00:00 2001 From: stopstone Date: Tue, 4 Nov 2025 17:24:58 +0900 Subject: [PATCH 24/95] =?UTF-8?q?refactor:=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=83=89=EC=83=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../textfield/internal/NearSearchTextFieldDecorationBox.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt index 04b3ac0b..04a0a72e 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt @@ -66,6 +66,7 @@ internal fun NearSearchTextFieldDecorationBox( painter = painterResource(id = R.drawable.ic_24_search), contentDescription = "검색", modifier = Modifier.size(24.dp), + tint = NearTheme.colors.GRAY01_888888, ) } } From a48bbe448cd78072da60af2484b70bec1e0f074c Mon Sep 17 00:00:00 2001 From: stopstone Date: Tue, 4 Nov 2025 17:29:16 +0900 Subject: [PATCH 25/95] =?UTF-8?q?refactor:=20=EC=97=B0=EB=9D=BD=EC=B2=98?= =?UTF-8?q?=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ContactLoadContent.kt | 2 +- .../main/res/drawable/img_64_user_gray.xml | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 Near/app/src/main/res/drawable/img_64_user_gray.xml diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactLoadContent.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactLoadContent.kt index d6eed8a4..0d57214e 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactLoadContent.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactLoadContent.kt @@ -117,7 +117,7 @@ fun FriendListItem( modifier = Modifier.weight(1f), ) { Image( - painter = painterResource(R.drawable.img_100_character_default), + painter = painterResource(R.drawable.img_64_user_gray), contentDescription = null, modifier = Modifier.size(24.dp), ) diff --git a/Near/app/src/main/res/drawable/img_64_user_gray.xml b/Near/app/src/main/res/drawable/img_64_user_gray.xml new file mode 100644 index 00000000..dab3a810 --- /dev/null +++ b/Near/app/src/main/res/drawable/img_64_user_gray.xml @@ -0,0 +1,24 @@ + + + + + + + + + From 8409fed15046d78125d05482b386a8bf34095f4a Mon Sep 17 00:00:00 2001 From: stopstone Date: Wed, 5 Nov 2025 12:52:52 +0900 Subject: [PATCH 26/95] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=B2=88=EB=8B=AC?= =?UTF-8?q?=20=EC=B1=99=EA=B8=B8=20=EC=82=AC=EB=9E=8C=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EA=B0=84=EA=B2=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 월별 리마인더 전체보기 화면의 카드 사이에 16dp의 간격을 추가했습니다. --- .../feature/mothlyreminderall/MonthlyReminderAllScreen.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index b957f845..51521e1f 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -122,12 +122,13 @@ internal fun MonthlyReminderAllScreen( Modifier .fillMaxSize() .padding(horizontal = 20.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), ) { items( items = uiState.monthlyReminders, key = { "monthly_${it.friendId}" }, ) { reminder -> + Spacer(modifier = Modifier.size(16.dp)) + MonthlyReminderFriendCard( reminder = reminder, onRecordClick = onRecordFriendShip, @@ -136,13 +137,14 @@ internal fun MonthlyReminderAllScreen( if (uiState.hasCompletedReminders) { item { - Spacer(modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.size(32.dp)) Text( text = stringResource(R.string.monthly_reminder_all_completed_section_title), style = NearTheme.typography.B2_14_BOLD, color = NearTheme.colors.BLACK_1A1A1A, ) + Spacer(modifier = Modifier.size(16.dp)) } items( From 3b8db78fb59b86e3bdbe315b264f98f96af3ad79 Mon Sep 17 00:00:00 2001 From: stopstone Date: Wed, 5 Nov 2025 21:17:00 +0900 Subject: [PATCH 27/95] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=88=EB=8B=AC=20?= =?UTF-8?q?=EC=B1=99=EA=B8=B8=20=EC=82=AC=EB=9E=8C=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 월별 리마인더 목록을 `nextContactAt`(다음 연락일) 기준으로 오름차순 정렬하여 표시하도록 수정했습니다. --- .../MonthlyReminderAllScreen.kt | 3 ++- .../MonthlyReminderAllViewModel.kt | 25 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index 51521e1f..25cf7cdb 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -1,7 +1,6 @@ package com.alarmy.near.presentation.feature.mothlyreminderall import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -152,6 +151,8 @@ internal fun MonthlyReminderAllScreen( key = { "completed_${it.friendId}" }, ) { reminder -> MonthlyReminderComplete(reminder = reminder) + + Spacer(modifier = Modifier.size(16.dp)) } } } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index 4a260145..93f227b2 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -47,7 +47,7 @@ class MonthlyReminderAllViewModel friendRepository .fetchMonthlyFriends() .onEach { monthlyFriends -> - val uiModels = convertToUIModels(monthlyFriends) + val uiModels = convertToUIModels(monthlyFriends).sortedBy { it.nextContactAt } _monthlyReminders.value = uiModels updateUIState() }.catch { exception -> @@ -77,18 +77,18 @@ class MonthlyReminderAllViewModel val completedFriendIds = completedList.map { it.friendId }.toSet() val filteredMonthlyList = monthlyList.filter { it.friendId !in completedFriendIds } - _uiState.update { - if (filteredMonthlyList.isEmpty() && completedList.isEmpty()) { - MonthlyReminderAllUIState.Empty - } else { - MonthlyReminderAllUIState.Success( - monthlyReminders = filteredMonthlyList, - completedReminders = completedList, - hasCompletedReminders = completedList.isNotEmpty(), - ) + _uiState.update { + if (filteredMonthlyList.isEmpty() && completedList.isEmpty()) { + MonthlyReminderAllUIState.Empty + } else { + MonthlyReminderAllUIState.Success( + monthlyReminders = filteredMonthlyList, + completedReminders = completedList, + hasCompletedReminders = completedList.isNotEmpty(), + ) + } } } - } fun onRecordFriendShip(friendId: String) { friendRepository @@ -99,7 +99,8 @@ class MonthlyReminderAllViewModel } val recordedFriend = _monthlyReminders.value.find { it.friendId == friendId } if (recordedFriend != null) { - _monthlyReminders.value = _monthlyReminders.value.filter { it.friendId != friendId } + _monthlyReminders.value = + _monthlyReminders.value.filter { it.friendId != friendId } _completedReminders.value = listOf(recordedFriend) + _completedReminders.value updateUIState() } From 2e25c7ebc2b0bcc3b988a26cc3d04a4e06f7e905 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 6 Nov 2025 01:39:05 +0900 Subject: [PATCH 28/95] =?UTF-8?q?feat:=20shimmer=20modifier=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/extension/NearShimmer.kt | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt new file mode 100644 index 00000000..e418a6ed --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt @@ -0,0 +1,101 @@ +package com.alarmy.near.presentation.ui.extension + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +internal enum class ShimmerType( + val colors: List, +) { + PRIMARY( + listOf( + Color(0xff5997E5), + Color(0xffA9D8FF), + ), + ), + WHITE( + listOf( + Color(0xffFFFFFF), + Color(0xffEBEBEB), + ), + ), +} + +@Composable +fun NearConditionalShimmer( + enabled: Boolean, + modifier: Modifier = Modifier, + shimmerColors: List = ShimmerType.PRIMARY.colors, + cornerRadius: Dp = 12.dp, + content: @Composable () -> Unit, +) { + Box(modifier = modifier) { + // 항상 content를 렌더링해서 크기 확보 (투명하게) + Box( + modifier = Modifier.alpha(if (enabled) 0f else 1f), + ) { + content() + } + + // shimmer가 enabled일 때만 효과 표시 + if (enabled) { + NearShimmerEffect( + modifier = Modifier.matchParentSize(), + shimmerColors = shimmerColors, + cornerRadius = cornerRadius, + ) + } + } +} + +@Composable +private fun NearShimmerEffect( + modifier: Modifier = Modifier, + shimmerColors: List = ShimmerType.PRIMARY.colors, + cornerRadius: Dp = 12.dp, +) { + val transition = rememberInfiniteTransition(label = "shimmer") + val translateAnim = + transition.animateFloat( + initialValue = 0f, + targetValue = 1000f, + animationSpec = + infiniteRepeatable( + animation = tween(durationMillis = 1000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "shimmer_translate", + ) + + val brush = + remember(translateAnim.value) { + Brush.linearGradient( + colors = shimmerColors, + start = Offset.Zero, + end = Offset(translateAnim.value, translateAnim.value), + ) + } + + Box( + modifier = + modifier + .clip(RoundedCornerShape(cornerRadius)) + .background(brush), + ) +} From ccf39de761eefaa6ba5fa8271e1e81c8a7909f61 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 6 Nov 2025 01:41:46 +0900 Subject: [PATCH 29/95] =?UTF-8?q?fix:=20=EA=B0=80=EB=A1=9C=20shimmer?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/extension/NearShimmer.kt | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt index e418a6ed..0c2711fb 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt @@ -1,6 +1,7 @@ package com.alarmy.near.presentation.ui.extension import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable @@ -10,13 +11,18 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -70,32 +76,41 @@ private fun NearShimmerEffect( shimmerColors: List = ShimmerType.PRIMARY.colors, cornerRadius: Dp = 12.dp, ) { + var componentWidth by remember { mutableFloatStateOf(0f) } + val transition = rememberInfiniteTransition(label = "shimmer") val translateAnim = transition.animateFloat( initialValue = 0f, - targetValue = 1000f, + targetValue = 1f, animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1000, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse, + animation = tween(durationMillis = 1000, easing = LinearEasing), + repeatMode = RepeatMode.Restart, ), label = "shimmer_translate", ) val brush = - remember(translateAnim.value) { - Brush.linearGradient( - colors = shimmerColors, - start = Offset.Zero, - end = Offset(translateAnim.value, translateAnim.value), - ) + remember(translateAnim.value, componentWidth, shimmerColors) { + if (componentWidth > 0f) { + val offset = translateAnim.value * componentWidth * 2 - componentWidth + Brush.linearGradient( + colors = shimmerColors, + start = Offset(x = offset, y = 0f), + end = Offset(x = offset + componentWidth, y = 0f), + ) + } else { + Brush.linearGradient(colors = shimmerColors) + } } Box( modifier = modifier - .clip(RoundedCornerShape(cornerRadius)) + .onGloballyPositioned { coordinates -> + componentWidth = coordinates.size.width.toFloat() + }.clip(RoundedCornerShape(cornerRadius)) .background(brush), ) } From c338a9ba3efc0bd20ab289bc18fa9a43a6abfa1e Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 6 Nov 2025 01:42:09 +0900 Subject: [PATCH 30/95] =?UTF-8?q?refactor:=20shimmer=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EB=B0=8F=20home=20uiState=EB=A1=9C=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/feature/home/HomeScreen.kt | 432 ++++++++++-------- .../feature/home/HomeViewModel.kt | 99 ++-- .../feature/home/model/HomeUiState.kt | 26 +- 3 files changed, 312 insertions(+), 245 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 6307c964..4b8f6ed0 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -52,16 +52,19 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.alarmy.near.R import com.alarmy.near.model.friendsummary.ContactFrequencyLevel import com.alarmy.near.model.friendsummary.FriendSummary -import com.alarmy.near.model.member.MemberInfo import com.alarmy.near.model.monthly.MonthlyFriend import com.alarmy.near.model.monthly.MonthlyFriendType import com.alarmy.near.presentation.feature.home.component.MyContacts +import com.alarmy.near.presentation.feature.home.model.HomeUiState +import com.alarmy.near.presentation.feature.home.model.MonthlyFriendUIState +import com.alarmy.near.presentation.feature.home.model.MyFriendUIState import com.alarmy.near.presentation.ui.component.dropdown.NearDropdownMenu import com.alarmy.near.presentation.ui.component.dropdown.NearDropdownMenuItem +import com.alarmy.near.presentation.ui.extension.NearConditionalShimmer +import com.alarmy.near.presentation.ui.extension.ShimmerType import com.alarmy.near.presentation.ui.extension.dropShadow import com.alarmy.near.presentation.ui.extension.onNoRippleClick import com.alarmy.near.presentation.ui.theme.NearTheme -import kotlinx.coroutines.launch import java.time.LocalDate private const val MINIMUM_PAGE_COUNT_TO_SHOW_UI = 2 @@ -76,23 +79,17 @@ internal fun HomeRoute( onAddContactClick: () -> Unit = {}, ) { LaunchedEffect(Unit) { - launch { - viewModel.errorEvent.collect { - onShowErrorSnackBar(IllegalStateException("네트워크 에러가 발생했습니다.")) - } + viewModel.errorEvent.collect { + onShowErrorSnackBar(IllegalStateException("네트워크 에러가 발생했습니다.")) } } - val memberInfo = viewModel.memberInfoFlow.collectAsStateWithLifecycle() - val friends = viewModel.friendsFlow.collectAsStateWithLifecycle() - val monthlyFriends = viewModel.monthlyFriendFlow.collectAsStateWithLifecycle() + val uiState = viewModel.uiState.collectAsStateWithLifecycle() HomeScreen( + uiState = uiState.value, onContactClick = onContactClick, onAlarmClick = onAlarmClick, onMyPageClick = onMyPageClick, onAddContactClick = onAddContactClick, - contacts = friends.value, - monthlyFriends = monthlyFriends.value, - memberInfo = memberInfo.value, ) } @@ -104,21 +101,10 @@ internal fun HomeScreen( onMyPageClick: () -> Unit = {}, onAlarmClick: () -> Unit = {}, onAddContactClick: () -> Unit = {}, - memberInfo: MemberInfo?, - contacts: List, - monthlyFriends: List, + uiState: HomeUiState, ) { val density = LocalDensity.current val statusBarHeightDp = with(density) { WindowInsets.statusBars.getTop(density).toDp() } - val contactsWithPage = contacts.chunked(5) - val pagerState: PagerState = - rememberPagerState( - initialPage = 0, - pageCount = { - contactsWithPage.count() + if (contactsWithPage.lastOrNull()?.count() == 5) 1 else 0 - }, - ) - val dropdownState = remember { mutableStateOf(false) } Surface(modifier = modifier) { Column( @@ -130,7 +116,8 @@ internal fun HomeScreen( R.drawable.img_bg, ), contentScale = ContentScale.FillBounds, - ).fillMaxSize(), + ) + .fillMaxSize(), ) { Spacer(modifier = Modifier.height(statusBarHeightDp)) Row( @@ -156,23 +143,24 @@ internal fun HomeScreen( ) } Spacer(modifier = Modifier.height(16.dp)) - Text( - text = - buildAnnotatedString { - append("${memberInfo?.nickname}님,\n") - withStyle( - SpanStyle( - fontWeight = FontWeight.Bold, - ), - ) { - append("누구를 챙길지") - } - append(" 정해볼까요?") - }, - modifier = Modifier.padding(horizontal = 24.dp), - style = NearTheme.typography.H1_24_REGULAR, - color = NearTheme.colors.WHITE_FFFFFF, - ) + + Box(modifier = Modifier.padding(horizontal = 24.dp)) { + NearConditionalShimmer(enabled = uiState.memberInfo == null) { + Text( + text = + buildAnnotatedString { + append("${uiState.memberInfo?.nickname}님,\n") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append("누구를 챙길지") + } + append(" 정해볼까요?") + }, + style = NearTheme.typography.H1_24_REGULAR, + color = NearTheme.colors.WHITE_FFFFFF, + ) + } + } + Spacer(modifier = Modifier.height(32.dp)) Text( text = stringResource(R.string.home_this_month_people), @@ -181,90 +169,103 @@ internal fun HomeScreen( color = NearTheme.colors.WHITE_FFFFFF, ) Spacer(modifier = Modifier.height(16.dp)) - if (monthlyFriends.isEmpty()) { - Surface( + + if (uiState.monthlyFriendUIState is MonthlyFriendUIState.Loading) { + NearConditionalShimmer( + enabled = true, modifier = Modifier - .fillMaxWidth() .padding(horizontal = 20.dp) - .clip(RoundedCornerShape(12.dp)), - color = NearTheme.colors.WHITE_FFFFFF.copy(alpha = 0.2f), - ) { - Text( - text = stringResource(R.string.home_no_people_this_month), + .fillMaxWidth() + .height(48.dp), + ) {} + } else if (uiState.monthlyFriendUIState is MonthlyFriendUIState.Success) { + if (uiState.monthlyFriendUIState.monthlyFriends.isEmpty()) { + Surface( modifier = Modifier .fillMaxWidth() - .padding(vertical = 14.dp), - textAlign = TextAlign.Center, - style = - NearTheme.typography.B2_14_MEDIUM.copy( - fontWeight = FontWeight.Normal, - ), - color = NearTheme.colors.WHITE_FFFFFF, - ) - } - } else { - LazyRow( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - items( - count = monthlyFriends.size, - key = { - monthlyFriends[it].friendId - }, + .padding(horizontal = 20.dp) + .clip(RoundedCornerShape(12.dp)), + color = NearTheme.colors.WHITE_FFFFFF.copy(alpha = 0.2f), ) { - val monthlyContact = monthlyFriends[it] - val now = LocalDate.now() - Surface( - modifier.dropShadow( - shape = RoundedCornerShape(12.dp), - color = Color.Black.copy(alpha = 0.07f), - blur = 4.dp, - offsetY = 4.dp, - ), + Text( + text = stringResource(R.string.home_no_people_this_month), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 14.dp), + textAlign = TextAlign.Center, + style = + NearTheme.typography.B2_14_MEDIUM.copy( + fontWeight = FontWeight.Normal, + ), color = NearTheme.colors.WHITE_FFFFFF, - shape = RoundedCornerShape(12.dp), + ) + } + } else { + val monthlyFriends = uiState.monthlyFriendUIState.monthlyFriends + LazyRow( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + count = monthlyFriends.size, + key = { + monthlyFriends[it].friendId + }, ) { - Row( - modifier = - Modifier - .padding(start = 12.dp, end = 16.dp) - .padding(vertical = 12.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + val monthlyContact = monthlyFriends[it] + val now = LocalDate.now() + Surface( + modifier.dropShadow( + shape = RoundedCornerShape(12.dp), + color = Color.Black.copy(alpha = 0.07f), + blur = 4.dp, + offsetY = 4.dp, + ), + color = NearTheme.colors.WHITE_FFFFFF, + shape = RoundedCornerShape(12.dp), ) { - Image( - painterResource(monthlyContact.type.imageSrc), - contentDescription = "", - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - modifier = Modifier.widthIn(max = 97.dp), - text = monthlyContact.name, - style = NearTheme.typography.B2_14_BOLD, - textAlign = TextAlign.Center, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - color = NearTheme.colors.BLACK_1A1A1A, - ) - Spacer(modifier = Modifier.width(12.dp)) - if (monthlyContact.isNextContactDay(now)) { - Text( - text = monthlyContact.daysUntilNextContact(LocalDate.now()), - style = NearTheme.typography.B2_14_BOLD, - color = NearTheme.colors.BLUE01_5AA2E9, + Row( + modifier = + Modifier + .padding(start = 12.dp, end = 16.dp) + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painterResource(monthlyContact.type.imageSrc), + contentDescription = "", ) - } else { + Spacer(modifier = Modifier.width(8.dp)) Text( - text = monthlyContact.daysUntilNextContact(now), - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.BLACK_1A1A1A.copy(alpha = 0.5f), + modifier = Modifier.widthIn(max = 97.dp), + text = monthlyContact.name, + style = NearTheme.typography.B2_14_BOLD, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + color = NearTheme.colors.BLACK_1A1A1A, ) + Spacer(modifier = Modifier.width(12.dp)) + if (monthlyContact.isNextContactDay(now)) { + Text( + text = monthlyContact.daysUntilNextContact(LocalDate.now()), + style = NearTheme.typography.B2_14_BOLD, + color = NearTheme.colors.BLUE01_5AA2E9, + ) + } else { + Text( + text = monthlyContact.daysUntilNextContact(now), + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.BLACK_1A1A1A.copy(alpha = 0.5f), + ) + } } } } @@ -274,68 +275,116 @@ internal fun HomeScreen( Spacer(modifier = Modifier.height(24.dp)) - Box( - modifier = - Modifier - .fillMaxSize() - .background( - color = NearTheme.colors.WHITE_FFFFFF, - shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), - ), - ) { - MyContacts( - modifier = Modifier.align(Alignment.TopCenter), - contactsWithPage = contactsWithPage, - pagerState = pagerState, - onContactClick = onContactClick, - onAddContactClick = { - onAddContactClick() - }, - ) - Row( + MyFriends( + onContactClick = onContactClick, + onAddContactClick = onAddContactClick, + myFriendUIState = uiState.myFriendUIState, + ) + } + } +} + +@Composable +private fun MyFriends( + myFriendUIState: MyFriendUIState, + onContactClick: (String) -> Unit, + onAddContactClick: () -> Unit, +) { + val contactsWithPage = + if (myFriendUIState is MyFriendUIState.Success) myFriendUIState.myFriends.chunked(5) else listOf() + val pagerState: PagerState = + rememberPagerState( + initialPage = 0, + pageCount = { + contactsWithPage.count() + + if (contactsWithPage + .lastOrNull() + ?.count() == 5 + ) { + 1 + } else { + 0 + } + }, + ) + val dropdownState = remember { mutableStateOf(false) } + + Box( + modifier = + Modifier + .fillMaxSize() + .background( + color = NearTheme.colors.WHITE_FFFFFF, + shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), + ), + ) { + if (myFriendUIState is MyFriendUIState.Loading) { + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(166.dp)) + NearConditionalShimmer( + shimmerColors = ShimmerType.WHITE.colors, + modifier = Modifier.size(width = 151.dp, height = 125.dp), + enabled = true, + ) { + } + } + } else { + MyContacts( + modifier = Modifier.align(Alignment.TopCenter), + contactsWithPage = contactsWithPage, + pagerState = pagerState, + onContactClick = onContactClick, + onAddContactClick = { + onAddContactClick() + }, + ) + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.TopStart) + .padding(top = 20.dp, start = 24.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + stringResource(R.string.home_my_people), + style = NearTheme.typography.H2_18_BOLD, + color = NearTheme.colors.BLACK_1A1A1A, + ) + Column { + Image( modifier = - Modifier - .fillMaxWidth() - .align(Alignment.TopStart) - .padding(top = 20.dp, start = 24.dp, end = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + Modifier.onNoRippleClick(onClick = { + dropdownState.value = true + }), + painter = painterResource(R.drawable.ic_32_menu), + contentDescription = stringResource(R.string.home_my_people_setting), + ) + NearDropdownMenu( + expanded = dropdownState.value, + onDismissRequest = { dropdownState.value = false }, ) { - Text( - stringResource(R.string.home_my_people), - style = NearTheme.typography.H2_18_BOLD, - color = NearTheme.colors.BLACK_1A1A1A, + NearDropdownMenuItem( + onClick = { + onAddContactClick() + dropdownState.value = false + }, + text = stringResource(R.string.home_menu_text_add_friend), ) - Column { - Image( - modifier = - Modifier.onNoRippleClick(onClick = { - dropdownState.value = true - }), - painter = painterResource(R.drawable.ic_32_menu), - contentDescription = stringResource(R.string.home_my_people_setting), - ) - NearDropdownMenu( - expanded = dropdownState.value, - onDismissRequest = { dropdownState.value = false }, - ) { - NearDropdownMenuItem( - onClick = { - onAddContactClick() - dropdownState.value = false - }, - text = stringResource(R.string.home_menu_text_add_friend), - ) - } - } } + } + } - if (contactsWithPage.size >= MINIMUM_PAGE_COUNT_TO_SHOW_UI) { - Column(modifier = Modifier.align(Alignment.BottomCenter)) { - PagerIndicator(pagerState) - Spacer(modifier = Modifier.height(104.dp)) - } - } + if (contactsWithPage.size >= MINIMUM_PAGE_COUNT_TO_SHOW_UI) { + Column(modifier = Modifier.align(Alignment.BottomCenter)) { + PagerIndicator(pagerState) + Spacer(modifier = Modifier.height(104.dp)) } } } @@ -376,34 +425,21 @@ internal fun HomeScreenPreview() { NearTheme { HomeScreen( onContactClick = {}, - contacts = - List(6) { - FriendSummary( - id = "2003", - name = "일이삼사오육칠팔구", - profileImageUrl = "https://search.yahoo.com/search?p=partiendo", - lastContactedAt = "2025-07-16", - isContacted = false, - contactFrequencyLevel = ContactFrequencyLevel.HIGH, - ) - }, - monthlyFriends = - List(4) { - MonthlyFriend( - friendId = "intellegat$it", - name = "Stacey Stewart", - type = MonthlyFriendType.ANNIVERSARY, - nextContactAt = "2025-09-30", - ) - }, - memberInfo = - MemberInfo( - memberId = "posidonium", - username = "주지스님", - nickname = "Audra Day", - imageUrl = "https://search.yahoo.com/search?p=class", - notificationAgreedAt = "comprehensam", - providerType = "sumo", + uiState = + HomeUiState( + myFriendUIState = + MyFriendUIState.Loading, + monthlyFriendUIState = + MonthlyFriendUIState.Success(List(4) { + MonthlyFriend( + friendId = "intellegat$it", + name = "Stacey Stewart", + type = MonthlyFriendType.ANNIVERSARY, + nextContactAt = "2025-09-30", + ) + },), + memberInfo = + null, ), ) } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt index d8681d00..c52e39c8 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt @@ -1,22 +1,21 @@ package com.alarmy.near.presentation.feature.home -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.alarmy.near.data.repository.FriendRepository import com.alarmy.near.data.repository.MemberRepository -import com.alarmy.near.model.friendsummary.FriendSummary -import com.alarmy.near.model.member.MemberInfo -import com.alarmy.near.model.monthly.MonthlyFriend +import com.alarmy.near.presentation.feature.home.model.HomeUiState +import com.alarmy.near.presentation.feature.home.model.MonthlyFriendUIState +import com.alarmy.near.presentation.feature.home.model.MyFriendUIState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -27,48 +26,62 @@ class HomeViewModel friendRepository: FriendRepository, memberRepository: MemberRepository, ) : ViewModel() { + private val _uiState = MutableStateFlow(HomeUiState()) + val uiState = _uiState.asStateFlow() + private val _errorEvent = Channel() private val deletedFriendIdsFlow = MutableStateFlow>(setOf()) val errorEvent = _errorEvent.receiveAsFlow() - val memberInfoFlow: StateFlow = - memberRepository - .getMyInfo() - .catch { - _errorEvent.send(it) - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - null, - ) - - val friendsFlow: StateFlow> = - combine( - friendRepository - .fetchFriends(), - deletedFriendIdsFlow, - ) { friends, deletedIds -> - friends.filter { it.id !in deletedIds } - }.catch { - _errorEvent.send(it) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) - val monthlyFriendFlow: - StateFlow> = - friendRepository - .fetchMonthlyFriends() - .catch { - _errorEvent.send(it) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) + init { + viewModelScope.launch { + launch { + memberRepository + .getMyInfo() + .catch { + _errorEvent.send(it) + }.collect { + _uiState.update { state -> + state.copy( + memberInfo = it, + ) + } + } + } + launch { + combine( + friendRepository + .fetchFriends(), + deletedFriendIdsFlow, + ) { friends, deletedIds -> + friends.filter { it.id !in deletedIds } + }.catch { + _errorEvent.send(it) + }.collect { + _uiState.update { state -> + state.copy( + myFriendUIState = MyFriendUIState.Success(it), + ) + } + } + } + launch { + friendRepository + .fetchMonthlyFriends() + .catch { + _errorEvent.send(it) + }.collect { + _uiState.update { state -> + state.copy( + monthlyFriendUIState = MonthlyFriendUIState.Success(it), + ) + } + } + } + } + } fun deleteFriend(friendId: String) { viewModelScope.launch { diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/model/HomeUiState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/model/HomeUiState.kt index c4510aef..5396dd0c 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/model/HomeUiState.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/model/HomeUiState.kt @@ -1,9 +1,27 @@ package com.alarmy.near.presentation.feature.home.model -sealed interface HomeUiState { - data object Loading : HomeUiState +import com.alarmy.near.model.friendsummary.FriendSummary +import com.alarmy.near.model.member.MemberInfo +import com.alarmy.near.model.monthly.MonthlyFriend +data class HomeUiState( + val memberInfo: MemberInfo? = null, + val monthlyFriendUIState: MonthlyFriendUIState = MonthlyFriendUIState.Loading, + val myFriendUIState: MyFriendUIState = MyFriendUIState.Loading, +) + +sealed interface MonthlyFriendUIState { + data class Success( + val monthlyFriends: List, + ) : MonthlyFriendUIState + + data object Loading : MonthlyFriendUIState +} + +sealed interface MyFriendUIState { data class Success( - val data: Any, - ) : HomeUiState + val myFriends: List, + ) : MyFriendUIState + + data object Loading : MyFriendUIState } From b334b283632bd8721935f3a77c082f125a55c24d Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 6 Nov 2025 01:45:14 +0900 Subject: [PATCH 31/95] =?UTF-8?q?fix:=20=EC=9D=B4=EB=B2=88=20=EB=8B=AC=20?= =?UTF-8?q?=EC=B1=99=EA=B8=B8=20=EC=82=AC=EB=9E=8C=20=EC=9A=B0=EC=B8=A1=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/alarmy/near/presentation/feature/home/HomeScreen.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 4b8f6ed0..4ae2cd8a 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -5,6 +5,7 @@ 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.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -206,10 +207,10 @@ internal fun HomeScreen( } else { val monthlyFriends = uiState.monthlyFriendUIState.monthlyFriends LazyRow( + contentPadding = PaddingValues(horizontal = 20.dp), modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), + .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { items( From b82da4d6fef77dda8212dd9815da5c015fc32b28 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 6 Nov 2025 01:48:09 +0900 Subject: [PATCH 32/95] =?UTF-8?q?chore:=20home=20preview=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/feature/home/HomeScreen.kt | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 4ae2cd8a..18f5d0d4 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -53,6 +53,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.alarmy.near.R import com.alarmy.near.model.friendsummary.ContactFrequencyLevel import com.alarmy.near.model.friendsummary.FriendSummary +import com.alarmy.near.model.member.MemberInfo import com.alarmy.near.model.monthly.MonthlyFriend import com.alarmy.near.model.monthly.MonthlyFriendType import com.alarmy.near.presentation.feature.home.component.MyContacts @@ -429,7 +430,18 @@ internal fun HomeScreenPreview() { uiState = HomeUiState( myFriendUIState = - MyFriendUIState.Loading, + MyFriendUIState.Success( + List(5) { + FriendSummary( + id = "2003", + name = "일이삼사오육칠팔구", + profileImageUrl = "https://search.yahoo.com/search?p=partiendo", + lastContactedAt = "2025-07-16", + isContacted = false, + contactFrequencyLevel = ContactFrequencyLevel.HIGH, + ) + }, + ), monthlyFriendUIState = MonthlyFriendUIState.Success(List(4) { MonthlyFriend( @@ -440,7 +452,14 @@ internal fun HomeScreenPreview() { ) },), memberInfo = - null, + MemberInfo( + memberId = "posidonium", + username = "주지스님", + nickname = "Audra Day", + imageUrl = "https://search.yahoo.com/search?p=class", + notificationAgreedAt = "comprehensam", + providerType = "sumo", + ), ), ) } From 3c61c80d1c20cd3f5b1b355f1be36db3853ca5c5 Mon Sep 17 00:00:00 2001 From: stopstone Date: Fri, 7 Nov 2025 15:54:58 +0900 Subject: [PATCH 33/95] =?UTF-8?q?refactor:=20=EC=9B=94=EB=B3=84=20?= =?UTF-8?q?=EB=A6=AC=EB=A7=88=EC=9D=B8=EB=8D=94=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=A1=9C=EB=94=A9=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `combine`을 사용하여 `fetchMonthlyFriends`와 `fetchMonthlyCompleteFriends` API 호출을 병렬로 처리하도록 수정했습니다. - 두 API의 응답을 모두 받은 후에 한 번에 UI 상태를 업데이트하여 데이터 로딩 중 발생할 수 있는 화면 깜빡임 현상을 개선했습니다. - API 응답을 함께 관리하기 위한 `MonthlyReminderCombinedData` data class를 추가했습니다. --- .../MonthlyReminderAllViewModel.kt | 53 +++++++++---------- .../model/MonthlyReminderCombinedData.kt | 8 +++ 2 files changed, 33 insertions(+), 28 deletions(-) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderCombinedData.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index 93f227b2..c027cf74 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.alarmy.near.data.repository.FriendRepository import com.alarmy.near.model.monthly.MonthlyFriend +import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderCombinedData import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderTypeInfo import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderUIModel import com.alarmy.near.presentation.feature.mothlyreminderall.uistate.MonthlyReminderAllUIEvent @@ -14,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow @@ -39,36 +41,31 @@ class MonthlyReminderAllViewModel private val _completedReminders = MutableStateFlow>(emptyList()) init { - fetchMonthlyFriends() - fetchMonthlyCompleteFriends() + fetchMonthlyReminders() } - private fun fetchMonthlyFriends() { - friendRepository - .fetchMonthlyFriends() - .onEach { monthlyFriends -> - val uiModels = convertToUIModels(monthlyFriends).sortedBy { it.nextContactAt } - _monthlyReminders.value = uiModels - updateUIState() - }.catch { exception -> - viewModelScope.launch { - _uiEvent.send(MonthlyReminderAllUIEvent.NetworkError) - } - }.launchIn(viewModelScope) - } - - private fun fetchMonthlyCompleteFriends() { - friendRepository - .fetchMonthlyCompleteFriends() - .onEach { monthlyFriends -> - val uiModels = convertToUIModels(monthlyFriends) - _completedReminders.value = uiModels - updateUIState() - }.catch { exception -> - viewModelScope.launch { - _uiEvent.send(MonthlyReminderAllUIEvent.NetworkError) - } - }.launchIn(viewModelScope) + private fun fetchMonthlyReminders() { + combine( + friendRepository.fetchMonthlyFriends(), + friendRepository.fetchMonthlyCompleteFriends(), + ) { monthlyFriends, completeFriends -> + MonthlyReminderCombinedData( + monthlyFriends = monthlyFriends, + completeFriends = completeFriends, + ) + }.onEach { data -> + val monthlyUIModels = + convertToUIModels(data.monthlyFriends).sortedBy { it.nextContactAt } + _monthlyReminders.value = monthlyUIModels + val completedUIModels = convertToUIModels(data.completeFriends) + _completedReminders.value = completedUIModels + // 두 데이터가 모두 준비된 후 UI 상태 업데이트 + updateUIState() + }.catch { exception -> + viewModelScope.launch { + _uiEvent.send(MonthlyReminderAllUIEvent.NetworkError) + } + }.launchIn(viewModelScope) } private fun updateUIState() { diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderCombinedData.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderCombinedData.kt new file mode 100644 index 00000000..2314f711 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderCombinedData.kt @@ -0,0 +1,8 @@ +package com.alarmy.near.presentation.feature.mothlyreminderall.model + +import com.alarmy.near.model.monthly.MonthlyFriend + +data class MonthlyReminderCombinedData( + val monthlyFriends: List, + val completeFriends: List, +) From 3cd540ba887601b59c3400824f82d044e769a33c Mon Sep 17 00:00:00 2001 From: stopstone Date: Fri, 7 Nov 2025 15:59:59 +0900 Subject: [PATCH 34/95] =?UTF-8?q?refactor:=20=ED=99=88=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20"=EC=A0=84=EC=B2=B4=20=EB=B3=B4=EA=B8=B0"=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alarmy/near/presentation/feature/home/HomeScreen.kt | 2 +- Near/app/src/main/res/values/strings.xml | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index f76d753e..65d1cc24 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -400,7 +400,7 @@ fun MonthlyReminderFriendsViewAll( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = "전체보기", + text = stringResource(R.string.home_monthly_friends_all), style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.WHITE_FFFFFF, modifier = Modifier.alpha(0.8f), diff --git a/Near/app/src/main/res/values/strings.xml b/Near/app/src/main/res/values/strings.xml index 0022b7e5..63b8b688 100644 --- a/Near/app/src/main/res/values/strings.xml +++ b/Near/app/src/main/res/values/strings.xml @@ -41,6 +41,7 @@ 가까워 지고 싶은 사람을\n추가해보세요. 사람 추가 사람 추가 + 전체 보기 전화걸기 @@ -107,7 +108,7 @@ 화면을 나가면 \n수정 내용은 저장되지 않아요. 확인 취소 - + 수정을 완료하시겠습니까? 저장 @@ -143,7 +144,7 @@ 편하게 의견을 남겨주세요. 그만두기 탈퇴하기 - + 탈퇴 시 계정 및 이용 내역이\n모두 삭제되며, 복구가 불가능합니다.\n정말 탈퇴하시겠습니까? 취소 @@ -156,7 +157,7 @@ 서비스 이용약관 개인정보 수집 및 이용동의 개인정보 처리방침 - + 서비스 약관 동의 약관 전체 동의 From 0f0ec163eea978c46bf9ac3dc34753874d241bf7 Mon Sep 17 00:00:00 2001 From: stopstone Date: Fri, 7 Nov 2025 16:02:08 +0900 Subject: [PATCH 35/95] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mothlyreminderall/MonthlyReminderAllScreen.kt | 10 ++-------- .../mothlyreminderall/MonthlyReminderAllViewModel.kt | 5 +++-- .../uistate/MonthlyReminderAllUIEvent.kt | 5 +++-- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index 25cf7cdb..f95ffdd9 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -41,7 +40,6 @@ fun MonthlyReminderAllRoute( onShowErrorSnackBar: (throwable: Throwable?) -> Unit, onNavigateBack: () -> Unit = {}, ) { - val context = LocalContext.current val uiState = viewModel.uiState.collectAsStateWithLifecycle() val recordSuccessDialogState = remember { mutableStateOf(false) } @@ -49,12 +47,8 @@ fun MonthlyReminderAllRoute( launch { viewModel.uiEvent.collect { event -> when (event) { - is MonthlyReminderAllUIEvent.NetworkError -> { - onShowErrorSnackBar( - IllegalStateException( - context.getString(R.string.monthly_reminder_all_network_error), - ), - ) + is MonthlyReminderAllUIEvent.ShowError -> { + onShowErrorSnackBar(event.throwable) } is MonthlyReminderAllUIEvent.RecordFriendShipSuccess -> { diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index c027cf74..b29d6dd5 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -63,7 +63,7 @@ class MonthlyReminderAllViewModel updateUIState() }.catch { exception -> viewModelScope.launch { - _uiEvent.send(MonthlyReminderAllUIEvent.NetworkError) + _uiEvent.send(MonthlyReminderAllUIEvent.ShowError(exception)) } }.launchIn(viewModelScope) } @@ -102,8 +102,9 @@ class MonthlyReminderAllViewModel updateUIState() } }.catch { exception -> + // 원본 exception을 전달하여 스택 트레이스와 디버깅 정보 유지 viewModelScope.launch { - _uiEvent.send(MonthlyReminderAllUIEvent.NetworkError) + _uiEvent.send(MonthlyReminderAllUIEvent.ShowError(exception)) } }.launchIn(viewModelScope) } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt index 494e6a56..889c234a 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/uistate/MonthlyReminderAllUIEvent.kt @@ -1,8 +1,9 @@ package com.alarmy.near.presentation.feature.mothlyreminderall.uistate sealed interface MonthlyReminderAllUIEvent { - data object NetworkError : MonthlyReminderAllUIEvent + data class ShowError( + val throwable: Throwable?, + ) : MonthlyReminderAllUIEvent data object RecordFriendShipSuccess : MonthlyReminderAllUIEvent } - From de9c02c18034823e4cd7ea139b52ba18cefade71 Mon Sep 17 00:00:00 2001 From: stopstone Date: Fri, 7 Nov 2025 16:06:12 +0900 Subject: [PATCH 36/95] =?UTF-8?q?refactor:=20Flow=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 반복적으로 사용되는 Flow의 `catch` 블록 내 에러 처리 로직을 `handleError` 확장 함수로 분리하여 공통화했습니다. - `MonthlyReminderAllViewModel` 내 `fetchMonthlyReminders`와 `onRecordFriendShip` 함수의 에러 처리 로직에 `handleError`를 적용하여 코드 중복을 제거하고 가독성을 개선했습니다. --- .../MonthlyReminderAllViewModel.kt | 85 +++++++++---------- .../near/utils/extensions/FlowExtensions.kt | 15 ++++ 2 files changed, 55 insertions(+), 45 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index b29d6dd5..66461486 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -9,12 +9,12 @@ import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyRemin import com.alarmy.near.presentation.feature.mothlyreminderall.model.MonthlyReminderUIModel import com.alarmy.near.presentation.feature.mothlyreminderall.uistate.MonthlyReminderAllUIEvent import com.alarmy.near.presentation.feature.mothlyreminderall.uistate.MonthlyReminderAllUIState +import com.alarmy.near.utils.extensions.handleError import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -44,29 +44,27 @@ class MonthlyReminderAllViewModel fetchMonthlyReminders() } - private fun fetchMonthlyReminders() { - combine( - friendRepository.fetchMonthlyFriends(), - friendRepository.fetchMonthlyCompleteFriends(), - ) { monthlyFriends, completeFriends -> - MonthlyReminderCombinedData( - monthlyFriends = monthlyFriends, - completeFriends = completeFriends, - ) - }.onEach { data -> - val monthlyUIModels = - convertToUIModels(data.monthlyFriends).sortedBy { it.nextContactAt } - _monthlyReminders.value = monthlyUIModels - val completedUIModels = convertToUIModels(data.completeFriends) - _completedReminders.value = completedUIModels - // 두 데이터가 모두 준비된 후 UI 상태 업데이트 - updateUIState() - }.catch { exception -> - viewModelScope.launch { - _uiEvent.send(MonthlyReminderAllUIEvent.ShowError(exception)) - } - }.launchIn(viewModelScope) - } + private fun fetchMonthlyReminders() { + combine( + friendRepository.fetchMonthlyFriends(), + friendRepository.fetchMonthlyCompleteFriends(), + ) { monthlyFriends, completeFriends -> + MonthlyReminderCombinedData( + monthlyFriends = monthlyFriends, + completeFriends = completeFriends, + ) + }.onEach { data -> + val monthlyUIModels = + convertToUIModels(data.monthlyFriends).sortedBy { it.nextContactAt } + _monthlyReminders.value = monthlyUIModels + val completedUIModels = convertToUIModels(data.completeFriends) + _completedReminders.value = completedUIModels + // 두 데이터가 모두 준비된 후 UI 상태 업데이트 + updateUIState() + }.handleError(viewModelScope, _uiEvent) { exception -> + MonthlyReminderAllUIEvent.ShowError(exception) + }.launchIn(viewModelScope) + } private fun updateUIState() { val monthlyList = _monthlyReminders.value @@ -87,27 +85,24 @@ class MonthlyReminderAllViewModel } } - fun onRecordFriendShip(friendId: String) { - friendRepository - .recordContact(friendId) - .onEach { _ -> - viewModelScope.launch { - _uiEvent.send(MonthlyReminderAllUIEvent.RecordFriendShipSuccess) - } - val recordedFriend = _monthlyReminders.value.find { it.friendId == friendId } - if (recordedFriend != null) { - _monthlyReminders.value = - _monthlyReminders.value.filter { it.friendId != friendId } - _completedReminders.value = listOf(recordedFriend) + _completedReminders.value - updateUIState() - } - }.catch { exception -> - // 원본 exception을 전달하여 스택 트레이스와 디버깅 정보 유지 - viewModelScope.launch { - _uiEvent.send(MonthlyReminderAllUIEvent.ShowError(exception)) - } - }.launchIn(viewModelScope) - } + fun onRecordFriendShip(friendId: String) { + friendRepository + .recordContact(friendId) + .onEach { _ -> + viewModelScope.launch { + _uiEvent.send(MonthlyReminderAllUIEvent.RecordFriendShipSuccess) + } + val recordedFriend = _monthlyReminders.value.find { it.friendId == friendId } + if (recordedFriend != null) { + _monthlyReminders.value = + _monthlyReminders.value.filter { it.friendId != friendId } + _completedReminders.value = listOf(recordedFriend) + _completedReminders.value + updateUIState() + } + }.handleError(viewModelScope, _uiEvent) { exception -> + MonthlyReminderAllUIEvent.ShowError(exception) + }.launchIn(viewModelScope) + } private fun convertToUIModels(monthlyFriends: List): List { val today = LocalDate.now() diff --git a/Near/app/src/main/java/com/alarmy/near/utils/extensions/FlowExtensions.kt b/Near/app/src/main/java/com/alarmy/near/utils/extensions/FlowExtensions.kt index 3bf0e2d7..51a1260e 100644 --- a/Near/app/src/main/java/com/alarmy/near/utils/extensions/FlowExtensions.kt +++ b/Near/app/src/main/java/com/alarmy/near/utils/extensions/FlowExtensions.kt @@ -1,9 +1,24 @@ package com.alarmy.near.utils.extensions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch inline fun apiCallFlow(crossinline apiCall: suspend () -> T): Flow = flow { emit(apiCall()) } + +fun Flow.handleError( + scope: CoroutineScope, + eventChannel: Channel, + createErrorEvent: (Throwable) -> E, +): Flow = + catch { exception -> + scope.launch { + eventChannel.send(createErrorEvent(exception)) + } + } From 42fabc6e406c631d86c197610bc22af7f08923d1 Mon Sep 17 00:00:00 2001 From: stopstone Date: Fri, 7 Nov 2025 16:10:00 +0900 Subject: [PATCH 37/95] =?UTF-8?q?refactor:=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=83=89=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `RecordSuccessDialog`에서 사용하던 하드코딩된 색상 값을 `NearTheme`의 색상으로 변경했습니다. - `NearColor`에 `BLACK_222222` 색상을 추가했습니다. --- .../mothlyreminderall/components/RecordSuccessDialog.kt | 3 +-- .../main/java/com/alarmy/near/presentation/ui/theme/Color.kt | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/RecordSuccessDialog.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/RecordSuccessDialog.kt index fedd5db9..c17fd226 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/RecordSuccessDialog.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/RecordSuccessDialog.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -50,7 +49,7 @@ fun RecordSuccessDialog(onDismiss: () -> Unit) { Text( stringResource(R.string.friend_profile_info_contact_success_text), style = NearTheme.typography.B1_16_BOLD, - color = Color(0xff222222), + color = NearTheme.colors.BLACK_222222, ) } } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/theme/Color.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/theme/Color.kt index 45d620b4..1cc38760 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/theme/Color.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/theme/Color.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.Color object NearColorPallete { val BLACK_1A1A1A = Color(0xFF1A1A1A) + val BLACK_222222 = Color(0xFF222222) val WHITE_FFFFFF = Color(0xFFFFFFFF) val GRAY01_888888 = Color(0xFF888888) val GRAY02_B7B7B7 = Color(0xFFB7B7B7) @@ -25,6 +26,7 @@ object NearColorPallete { @Suppress("PropertyName") data class NearColor( val BLACK_1A1A1A: Color = NearColorPallete.BLACK_1A1A1A, + val BLACK_222222: Color = NearColorPallete.BLACK_222222, val WHITE_FFFFFF: Color = NearColorPallete.WHITE_FFFFFF, val GRAY01_888888: Color = NearColorPallete.GRAY01_888888, val GRAY02_B7B7B7: Color = NearColorPallete.GRAY02_B7B7B7, From 61d61ac6e4054e346a96cdc5240600eaa3468cdb Mon Sep 17 00:00:00 2001 From: stopstone Date: Fri, 7 Nov 2025 16:17:44 +0900 Subject: [PATCH 38/95] =?UTF-8?q?chore:=20MonthlyReminderFriendCard.kt=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=8A=A4=ED=83=80=EC=9D=BC=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 --- .../components/MonthlyReminderFriendCard.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt index 526bcafc..25236792 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt @@ -79,15 +79,15 @@ fun MonthlyReminderFriendCard( color = NearTheme.colors.BLACK_1A1A1A, ) - Spacer(modifier = Modifier.size(6.dp)) + Spacer(modifier = Modifier.size(6.dp)) - Text( - text = stringResource(reminder.descriptionRes), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.GRAY01_888888, - ) + Text( + text = stringResource(reminder.descriptionRes), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.GRAY01_888888, + ) } Spacer(modifier = Modifier.size(10.dp)) From 31a0e66df64045b97bb2bdcbc059e5fd6d184ebe Mon Sep 17 00:00:00 2001 From: stopstone Date: Fri, 7 Nov 2025 16:20:04 +0900 Subject: [PATCH 39/95] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=B2=88=EB=8B=AC?= =?UTF-8?q?=20=EC=B1=99=EA=B8=B8=EC=82=AC=EB=9E=8C=20=ED=95=98=EB=8B=A8=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/mothlyreminderall/MonthlyReminderAllScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index f95ffdd9..4302fd1b 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -2,6 +2,7 @@ package com.alarmy.near.presentation.feature.mothlyreminderall import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -115,6 +116,7 @@ internal fun MonthlyReminderAllScreen( Modifier .fillMaxSize() .padding(horizontal = 20.dp), + contentPadding = PaddingValues(bottom = 80.dp), ) { items( items = uiState.monthlyReminders, @@ -150,8 +152,6 @@ internal fun MonthlyReminderAllScreen( } } } - - Spacer(modifier = Modifier.size(80.dp)) } } } From 1672ffaa89127b82438d2239321047f587a9dec9 Mon Sep 17 00:00:00 2001 From: stopstone Date: Fri, 7 Nov 2025 16:46:38 +0900 Subject: [PATCH 40/95] =?UTF-8?q?chore:=20"D-day"=EB=A5=BC=20"D-DAY"?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 연락 주기까지 남은 날짜를 표시하는 문자열을 "D-day"에서 "D-DAY"로 변경했습니다. --- .../main/java/com/alarmy/near/model/monthly/MonthlyFriend.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/model/monthly/MonthlyFriend.kt b/Near/app/src/main/java/com/alarmy/near/model/monthly/MonthlyFriend.kt index ac4fbb1b..0006ed90 100644 --- a/Near/app/src/main/java/com/alarmy/near/model/monthly/MonthlyFriend.kt +++ b/Near/app/src/main/java/com/alarmy/near/model/monthly/MonthlyFriend.kt @@ -13,7 +13,7 @@ data class MonthlyFriend( fun daysUntilNextContact(today: LocalDate): String { val daysBetween = getDaysBetween(today) return when { - daysBetween == 0L -> "D-day" + daysBetween == 0L -> "D-DAY" daysBetween > 0L -> "D-$daysBetween" else -> "D+${-daysBetween}" // 과거 날짜 } From c9c432658888e71dfa0c6cbc104cb66cd69be28c Mon Sep 17 00:00:00 2001 From: stopstone Date: Fri, 7 Nov 2025 16:48:40 +0900 Subject: [PATCH 41/95] =?UTF-8?q?feat:=20=EC=97=B0=EB=9D=BD=EC=9D=BC?= =?UTF-8?q?=EC=9D=B4=20=EC=98=A4=EB=8A=98=EC=9D=BC=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?D-day=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A4=91=EB=B3=B5=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `MonthlyReminderUIModel`에 연락일이 오늘인지 여부를 나타내는 `isToday` 프로퍼티를 추가했습니다. - `isToday` 값에 따라 D-day 텍스트 스타일(파란색, 굵은 글씨)이 적용되도록 `MonthlyReminderFriendCard`를 수정했습니다. - 월간 리마인더 목록에서 중복된 친구 데이터가 표시되지 않도록 `distinctBy`를 사용하여 필터링 로직을 추가했습니다. --- .../MonthlyReminderAllScreen.kt | 5 +- .../MonthlyReminderAllViewModel.kt | 90 ++++++++++--------- .../components/MonthlyReminderComplete.kt | 1 + .../components/MonthlyReminderFriendCard.kt | 3 +- .../model/MonthlyReminderUIModel.kt | 1 + 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index 4302fd1b..9080ceec 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -173,6 +173,7 @@ private fun MonthlyReminderAllScreenPreview() { descriptionRes = R.string.monthly_reminder_all_type_birthday_description, nextContactAt = "2025-11-05", daysUntilNextContact = "D-4", + isToday = false, ), MonthlyReminderUIModel( friendId = "2", @@ -180,7 +181,8 @@ private fun MonthlyReminderAllScreenPreview() { imageRes = R.drawable.icon_visual_mail, descriptionRes = R.string.monthly_reminder_all_type_message_description, nextContactAt = "2025-11-01", - daysUntilNextContact = "D-DAY", + daysUntilNextContact = "D-day", + isToday = true, ), ), completedReminders = @@ -192,6 +194,7 @@ private fun MonthlyReminderAllScreenPreview() { descriptionRes = R.string.monthly_reminder_all_type_anniversary_description, nextContactAt = "2025-10-15", daysUntilNextContact = "D+16", + isToday = false, ), ), hasCompletedReminders = true, diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index 66461486..df789f29 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -44,33 +44,40 @@ class MonthlyReminderAllViewModel fetchMonthlyReminders() } - private fun fetchMonthlyReminders() { - combine( - friendRepository.fetchMonthlyFriends(), - friendRepository.fetchMonthlyCompleteFriends(), - ) { monthlyFriends, completeFriends -> - MonthlyReminderCombinedData( - monthlyFriends = monthlyFriends, - completeFriends = completeFriends, - ) - }.onEach { data -> - val monthlyUIModels = - convertToUIModels(data.monthlyFriends).sortedBy { it.nextContactAt } - _monthlyReminders.value = monthlyUIModels - val completedUIModels = convertToUIModels(data.completeFriends) - _completedReminders.value = completedUIModels - // 두 데이터가 모두 준비된 후 UI 상태 업데이트 - updateUIState() - }.handleError(viewModelScope, _uiEvent) { exception -> - MonthlyReminderAllUIEvent.ShowError(exception) - }.launchIn(viewModelScope) - } + private fun fetchMonthlyReminders() { + combine( + friendRepository.fetchMonthlyFriends(), + friendRepository.fetchMonthlyCompleteFriends(), + ) { monthlyFriends, completeFriends -> + MonthlyReminderCombinedData( + monthlyFriends = monthlyFriends, + completeFriends = completeFriends, + ) + }.onEach { data -> + val monthlyUIModels = + convertToUIModels(data.monthlyFriends) + .distinctBy { it.friendId } + .sortedBy { it.nextContactAt } + _monthlyReminders.value = monthlyUIModels + val completedUIModels = + convertToUIModels(data.completeFriends) + .distinctBy { it.friendId } + _completedReminders.value = completedUIModels + // 두 데이터가 모두 준비된 후 UI 상태 업데이트 + updateUIState() + }.handleError(viewModelScope, _uiEvent) { exception -> + MonthlyReminderAllUIEvent.ShowError(exception) + }.launchIn(viewModelScope) + } private fun updateUIState() { val monthlyList = _monthlyReminders.value val completedList = _completedReminders.value val completedFriendIds = completedList.map { it.friendId }.toSet() - val filteredMonthlyList = monthlyList.filter { it.friendId !in completedFriendIds } + val filteredMonthlyList = + monthlyList + .filter { it.friendId !in completedFriendIds } + .distinctBy { it.friendId } _uiState.update { if (filteredMonthlyList.isEmpty() && completedList.isEmpty()) { @@ -78,31 +85,31 @@ class MonthlyReminderAllViewModel } else { MonthlyReminderAllUIState.Success( monthlyReminders = filteredMonthlyList, - completedReminders = completedList, + completedReminders = completedList.distinctBy { it.friendId }, hasCompletedReminders = completedList.isNotEmpty(), ) } } } - fun onRecordFriendShip(friendId: String) { - friendRepository - .recordContact(friendId) - .onEach { _ -> - viewModelScope.launch { - _uiEvent.send(MonthlyReminderAllUIEvent.RecordFriendShipSuccess) - } - val recordedFriend = _monthlyReminders.value.find { it.friendId == friendId } - if (recordedFriend != null) { - _monthlyReminders.value = - _monthlyReminders.value.filter { it.friendId != friendId } - _completedReminders.value = listOf(recordedFriend) + _completedReminders.value - updateUIState() - } - }.handleError(viewModelScope, _uiEvent) { exception -> - MonthlyReminderAllUIEvent.ShowError(exception) - }.launchIn(viewModelScope) - } + fun onRecordFriendShip(friendId: String) { + friendRepository + .recordContact(friendId) + .onEach { _ -> + viewModelScope.launch { + _uiEvent.send(MonthlyReminderAllUIEvent.RecordFriendShipSuccess) + } + val recordedFriend = _monthlyReminders.value.find { it.friendId == friendId } + if (recordedFriend != null) { + _monthlyReminders.value = + _monthlyReminders.value.filter { it.friendId != friendId } + _completedReminders.value = listOf(recordedFriend) + _completedReminders.value + updateUIState() + } + }.handleError(viewModelScope, _uiEvent) { exception -> + MonthlyReminderAllUIEvent.ShowError(exception) + }.launchIn(viewModelScope) + } private fun convertToUIModels(monthlyFriends: List): List { val today = LocalDate.now() @@ -116,6 +123,7 @@ class MonthlyReminderAllViewModel descriptionRes = typeInfo.descriptionRes, nextContactAt = friend.nextContactAt, daysUntilNextContact = friend.daysUntilNextContact(today), + isToday = friend.isNextContactDay(today), ) } catch (e: Exception) { null diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt index 8b610039..95a4a0d3 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderComplete.kt @@ -79,6 +79,7 @@ fun MonthlyReminderCompletePreview() { descriptionRes = R.string.monthly_reminder_all_type_birthday_description, nextContactAt = "2025-03-20", daysUntilNextContact = "D-9", + isToday = false, ), ) } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt index 25236792..1d33184e 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/components/MonthlyReminderFriendCard.kt @@ -93,7 +93,7 @@ fun MonthlyReminderFriendCard( Spacer(modifier = Modifier.size(10.dp)) val (textStyle, textColor) = - if (reminder.daysUntilNextContact == "D-DAY") { + if (reminder.isToday) { NearTheme.typography.B2_14_BOLD to NearTheme.colors.BLUE01_5AA2E9 } else { NearTheme.typography.B2_14_MEDIUM to NearTheme.colors.GRAY01_888888 @@ -140,6 +140,7 @@ fun MonthlyReminderFriendCardPreview() { descriptionRes = R.string.monthly_reminder_all_type_birthday_description, nextContactAt = "2025-11-05", daysUntilNextContact = "D-DAY", + isToday = true, ), ) } diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt index 1a8b4ad7..74f82d6e 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/model/MonthlyReminderUIModel.kt @@ -10,5 +10,6 @@ data class MonthlyReminderUIModel( @StringRes val descriptionRes: Int, val nextContactAt: String, val daysUntilNextContact: String, + val isToday: Boolean, ) From be854844d246c437d3709b4bee00c8592b96c9e4 Mon Sep 17 00:00:00 2001 From: stopstone Date: Tue, 11 Nov 2025 00:43:28 +0900 Subject: [PATCH 42/95] =?UTF-8?q?refactor:=20MonthlyReminderAllScreen?= =?UTF-8?q?=EC=9D=98=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20launch=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `LaunchedEffect` 내에서 `collect`를 사용할 때 불필요한 `launch` 블록을 제거하여 코드를 간소화했습니다. --- .../MonthlyReminderAllScreen.kt | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt index 9080ceec..c5ac6fae 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllScreen.kt @@ -33,7 +33,6 @@ import com.alarmy.near.presentation.feature.mothlyreminderall.uistate.MonthlyRem import com.alarmy.near.presentation.ui.component.NearFrame import com.alarmy.near.presentation.ui.component.appbar.NearTopAppbar import com.alarmy.near.presentation.ui.theme.NearTheme -import kotlinx.coroutines.launch @Composable fun MonthlyReminderAllRoute( @@ -44,17 +43,15 @@ fun MonthlyReminderAllRoute( val uiState = viewModel.uiState.collectAsStateWithLifecycle() val recordSuccessDialogState = remember { mutableStateOf(false) } - LaunchedEffect(viewModel.uiEvent) { - launch { - viewModel.uiEvent.collect { event -> - when (event) { - is MonthlyReminderAllUIEvent.ShowError -> { - onShowErrorSnackBar(event.throwable) - } + LaunchedEffect(Unit) { + viewModel.uiEvent.collect { event -> + when (event) { + is MonthlyReminderAllUIEvent.ShowError -> { + onShowErrorSnackBar(event.throwable) + } - is MonthlyReminderAllUIEvent.RecordFriendShipSuccess -> { - recordSuccessDialogState.value = true - } + is MonthlyReminderAllUIEvent.RecordFriendShipSuccess -> { + recordSuccessDialogState.value = true } } } From 186e13b89e42c8250984d0b8f3e213ed172523ee Mon Sep 17 00:00:00 2001 From: stopstone Date: Tue, 11 Nov 2025 00:44:26 +0900 Subject: [PATCH 43/95] =?UTF-8?q?refactor:=20MonthlyReminderAllViewModel?= =?UTF-8?q?=20=EB=82=B4=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20private?= =?UTF-8?q?=20backing=20property=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `_monthlyReminders`와 `_completedReminders`에 대한 불필요한 `_` 접두사를 제거하여 코드를 간소화했습니다. --- .../MonthlyReminderAllViewModel.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index df789f29..d31e72d8 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -37,8 +37,8 @@ class MonthlyReminderAllViewModel MutableStateFlow(MonthlyReminderAllUIState.Loading) val uiState: StateFlow = _uiState.asStateFlow() - private val _monthlyReminders = MutableStateFlow>(emptyList()) - private val _completedReminders = MutableStateFlow>(emptyList()) + private val monthlyReminders = MutableStateFlow>(emptyList()) + private val completedReminders = MutableStateFlow>(emptyList()) init { fetchMonthlyReminders() @@ -58,11 +58,11 @@ class MonthlyReminderAllViewModel convertToUIModels(data.monthlyFriends) .distinctBy { it.friendId } .sortedBy { it.nextContactAt } - _monthlyReminders.value = monthlyUIModels + monthlyReminders.value = monthlyUIModels val completedUIModels = convertToUIModels(data.completeFriends) .distinctBy { it.friendId } - _completedReminders.value = completedUIModels + completedReminders.value = completedUIModels // 두 데이터가 모두 준비된 후 UI 상태 업데이트 updateUIState() }.handleError(viewModelScope, _uiEvent) { exception -> @@ -71,8 +71,8 @@ class MonthlyReminderAllViewModel } private fun updateUIState() { - val monthlyList = _monthlyReminders.value - val completedList = _completedReminders.value + val monthlyList = monthlyReminders.value + val completedList = completedReminders.value val completedFriendIds = completedList.map { it.friendId }.toSet() val filteredMonthlyList = monthlyList @@ -99,11 +99,11 @@ class MonthlyReminderAllViewModel viewModelScope.launch { _uiEvent.send(MonthlyReminderAllUIEvent.RecordFriendShipSuccess) } - val recordedFriend = _monthlyReminders.value.find { it.friendId == friendId } + val recordedFriend = monthlyReminders.value.find { it.friendId == friendId } if (recordedFriend != null) { - _monthlyReminders.value = - _monthlyReminders.value.filter { it.friendId != friendId } - _completedReminders.value = listOf(recordedFriend) + _completedReminders.value + monthlyReminders.value = + monthlyReminders.value.filter { it.friendId != friendId } + completedReminders.value = listOf(recordedFriend) + completedReminders.value updateUIState() } }.handleError(viewModelScope, _uiEvent) { exception -> From 49e421ed29b9231dd84f89e12aecda0b564a556d Mon Sep 17 00:00:00 2001 From: stopstone Date: Tue, 11 Nov 2025 00:47:55 +0900 Subject: [PATCH 44/95] =?UTF-8?q?refactor:=20updateUIState=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EC=9D=98=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `updateUIState` 함수의 역할을 더 명확하게 나타내기 위해 `combineRemindersToUIState`로 이름을 변경했습니다. --- .../mothlyreminderall/MonthlyReminderAllViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt index d31e72d8..08aa4205 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/mothlyreminderall/MonthlyReminderAllViewModel.kt @@ -64,13 +64,13 @@ class MonthlyReminderAllViewModel .distinctBy { it.friendId } completedReminders.value = completedUIModels // 두 데이터가 모두 준비된 후 UI 상태 업데이트 - updateUIState() + combineRemindersToUIState() }.handleError(viewModelScope, _uiEvent) { exception -> MonthlyReminderAllUIEvent.ShowError(exception) }.launchIn(viewModelScope) } - private fun updateUIState() { + private fun combineRemindersToUIState() { val monthlyList = monthlyReminders.value val completedList = completedReminders.value val completedFriendIds = completedList.map { it.friendId }.toSet() @@ -104,7 +104,7 @@ class MonthlyReminderAllViewModel monthlyReminders.value = monthlyReminders.value.filter { it.friendId != friendId } completedReminders.value = listOf(recordedFriend) + completedReminders.value - updateUIState() + combineRemindersToUIState() } }.handleError(viewModelScope, _uiEvent) { exception -> MonthlyReminderAllUIEvent.ShowError(exception) From 2cffb56fbd5c30a8ff2350bf78a61c3d77ce4190 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 01:31:10 +0900 Subject: [PATCH 45/95] =?UTF-8?q?fix:=20inset=20=ED=8C=A8=EB=94=A9=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EC=9E=91=EC=9D=80=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=ED=99=94=EB=A9=B4=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/onboarding/OnboardingScreen.kt | 158 ++++++++++-------- 1 file changed, 84 insertions(+), 74 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt index 48928afc..e4f85707 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt @@ -6,12 +6,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -21,8 +23,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -40,6 +42,7 @@ import com.alarmy.near.presentation.feature.onboarding.components.BackgroundArea import com.alarmy.near.presentation.feature.onboarding.components.OnboardingButton import com.alarmy.near.presentation.feature.onboarding.components.PageIndicator import com.alarmy.near.presentation.feature.onboarding.model.OnboardingPage +import com.alarmy.near.presentation.ui.component.NearFrame import com.alarmy.near.presentation.ui.theme.NearTheme import kotlinx.coroutines.launch @@ -67,30 +70,31 @@ fun OnboardingScreen( } // 온보딩 페이지 데이터 - remember로 성능 최적화 - val pages = remember { - listOf( - OnboardingPage( - titleResId = R.string.first_onboarding_title, - image = R.drawable.img_onboarding_page_first, - ), - OnboardingPage( - titleResId = R.string.second_onboarding_title, - image = R.drawable.img_onboarding_page_second, - ), - OnboardingPage( - titleResId = R.string.third_onboarding_title, - image = R.drawable.img_onboarding_page_third, - ), - OnboardingPage( - titleResId = R.string.fourth_onboarding_title, - image = R.drawable.img_onboarding_page_forth, - ), - OnboardingPage( - titleResId = R.string.fifth_onboarding_title, - image = R.drawable.img_onboarding_page_fifth, - ), - ) - } + val pages = + remember { + listOf( + OnboardingPage( + titleResId = R.string.first_onboarding_title, + image = R.drawable.img_onboarding_page_first, + ), + OnboardingPage( + titleResId = R.string.second_onboarding_title, + image = R.drawable.img_onboarding_page_second, + ), + OnboardingPage( + titleResId = R.string.third_onboarding_title, + image = R.drawable.img_onboarding_page_third, + ), + OnboardingPage( + titleResId = R.string.fourth_onboarding_title, + image = R.drawable.img_onboarding_page_forth, + ), + OnboardingPage( + titleResId = R.string.fifth_onboarding_title, + image = R.drawable.img_onboarding_page_fifth, + ), + ) + } // 상태바와 네비게이션 바 높이 계산 val density = LocalDensity.current @@ -101,53 +105,56 @@ fun OnboardingScreen( val pagerState = rememberPagerState(pageCount = { pages.size }) val scope = rememberCoroutineScope() - Box( - modifier = Modifier.fillMaxSize(), - ) { - BackgroundArea() - Column( - modifier = Modifier - .padding(top = statusBarHeightDp, bottom = navigationBarHeightDp), - horizontalAlignment = Alignment.CenterHorizontally, + NearFrame { + Box( + modifier = Modifier.fillMaxSize(), ) { - // 뷰페이저 - HorizontalPager( - state = pagerState, - modifier = Modifier.weight(1f), - ) { page -> - OnboardingPageContent( - page = pages[page], - modifier = Modifier.fillMaxSize(), - ) - } + BackgroundArea() + Column( + modifier = + Modifier + .padding(top = statusBarHeightDp, bottom = navigationBarHeightDp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // 뷰페이저 + HorizontalPager( + state = pagerState, + ) { page -> + OnboardingPageContent( + page = pages[page], + modifier = Modifier.fillMaxWidth(), + ) + } - Spacer(modifier = Modifier.size(25.dp)) + Spacer(modifier = Modifier.size(24.dp)) - // 페이지 인디케이터 - PageIndicator( - pageCount = pages.size, - currentPage = pagerState.currentPage, - ) + // 페이지 인디케이터 + PageIndicator( + pageCount = pages.size, + currentPage = pagerState.currentPage, + ) - Spacer(modifier = Modifier.size(32.dp)) - - // 다음/완료 버튼 - OnboardingButton( - currentPage = pagerState.currentPage, - totalPages = pages.size, - isLoading = uiState.isLoading, - onNextClick = { - if (pagerState.currentPage < pages.size - 1) { - scope.launch { - pagerState.animateScrollToPage(pagerState.currentPage + 1) - } - } else { - // 온보딩 완료 시 DataStore에 저장 - viewModel.completeOnboarding() - } - }, - ) - Spacer(modifier = Modifier.size(24.dp)) + Spacer(modifier = Modifier.size(14.dp)) + } + Column(modifier = Modifier.align(Alignment.BottomCenter)) { + // 다음/완료 버튼 + OnboardingButton( + currentPage = pagerState.currentPage, + totalPages = pages.size, + isLoading = uiState.isLoading, + onNextClick = { + if (pagerState.currentPage < pages.size - 1) { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } else { + // 온보딩 완료 시 DataStore에 저장 + viewModel.completeOnboarding() + } + }, + ) + Spacer(modifier = Modifier.size(24.dp)) + } } } } @@ -165,7 +172,7 @@ private fun OnboardingPageContent( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(modifier = Modifier.size(45.dp)) + Spacer(modifier = Modifier.size(22.dp)) // 각 온보딩 페이지 타이틀 Text( @@ -178,15 +185,15 @@ private fun OnboardingPageContent( ), ) - Spacer(modifier = Modifier.size(24.dp)) + Spacer(modifier = Modifier.size(16.dp)) Image( - modifier = Modifier.weight(1f), + modifier = + Modifier + .clip(RoundedCornerShape(12.dp)), painter = painterResource(page.image), contentDescription = null, - contentScale = ContentScale.Crop, ) - } } @@ -217,7 +224,10 @@ private fun createAnnotatedText( /** * 지정된 색상으로 텍스트를 추가하는 함수 */ -private fun AnnotatedString.Builder.appendStyledText(text: String, color: Color) { +private fun AnnotatedString.Builder.appendStyledText( + text: String, + color: Color, +) { withStyle(style = SpanStyle(color = color)) { append(text) } From a0d5ed02eb3dbff9ab3de679b2d2436317a8465f Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 01:36:53 +0900 Subject: [PATCH 46/95] =?UTF-8?q?fix:=20login=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80,=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EA=B0=84=EA=B2=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/feature/login/LoginScreen.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt index cb786685..9c321d7e 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt @@ -21,15 +21,19 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.repeatOnLifecycle +import coil.size.Scale import com.alarmy.near.R import com.alarmy.near.model.ProviderType import com.alarmy.near.presentation.feature.login.auth.SocialLoginHandler @@ -164,8 +168,9 @@ private fun LoginIntroductionSection(modifier: Modifier = Modifier) { ) Image( - modifier = modifier.wrapContentSize(Alignment.Center), + modifier = modifier.height(48.dp), alignment = Alignment.Center, + contentScale = ContentScale.None, painter = painterResource(R.drawable.ic_near_logo_title_primary), contentDescription = stringResource(R.string.near_logo_title), ) @@ -173,7 +178,8 @@ private fun LoginIntroductionSection(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.size(LoginScreenConstants.DESCRIPTION_SPACING.dp)) Text( - modifier = modifier.wrapContentSize(Alignment.Center), + modifier = modifier, + textAlign = TextAlign.Center, text = stringResource(R.string.login_near_description), style = NearTheme.typography.B1_16_MEDIUM, color = NearTheme.colors.GRAY01_888888, @@ -228,3 +234,12 @@ private object LoginScreenConstants { const val DESCRIPTION_SPACING = 12 const val BOTTOM_SPACING = 96 } + +@Preview +@Composable +fun LoginScreenPreview() { + LoginScreen( + onLoginClick = {}, + uiState = LoginUiState(), + ) +} From 29bf984deda6ea838e2bd65120a80122df53ed99 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 01:41:05 +0900 Subject: [PATCH 47/95] =?UTF-8?q?fix:=20=EC=97=B0=EB=9D=BD=EC=B2=98=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=B2=84=ED=8A=BC=20=EC=83=89=EC=83=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NearSearchTextFieldDecorationBox.kt | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt index 04b3ac0b..c83affdd 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt @@ -33,39 +33,43 @@ internal fun NearSearchTextFieldDecorationBox( colors: TextFieldColors, placeHolderText: String, onSearchClick: () -> Unit, - contentPadding: PaddingValues = PaddingValues( - start = 16.dp, - top = 16.dp, - bottom = 16.dp, - end = 8.dp - ), + contentPadding: PaddingValues = + PaddingValues( + start = 16.dp, + top = 16.dp, + bottom = 16.dp, + end = 8.dp, + ), ) { OutlinedTextFieldDefaults.DecorationBox( contentPadding = contentPadding, value = value, innerTextField = { Box( - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) { Box( - modifier = Modifier - .fillMaxWidth() - .padding(end = 40.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(end = 40.dp), ) { innerTextField() } IconButton( onClick = onSearchClick, - modifier = Modifier - .align(Alignment.CenterEnd) - .size(32.dp), - enabled = enabled + modifier = + Modifier + .align(Alignment.CenterEnd) + .size(32.dp), + enabled = enabled, ) { Icon( painter = painterResource(id = R.drawable.ic_24_search), contentDescription = "검색", modifier = Modifier.size(24.dp), + tint = NearTheme.colors.GRAY01_888888, ) } } From 0182de2e568f19ea307f857d758f20b39b8fb34a Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 19:34:13 +0900 Subject: [PATCH 48/95] =?UTF-8?q?fix:=20home=20=EB=94=94=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EA=B8=B4=20=EA=B2=BD=EC=9A=B0=20=EC=A4=91=EA=B0=84?= =?UTF-8?q?=20=EB=A7=88=EC=A7=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 타이틀 ~ 이번달 챙길사람 패딩 작업 --- .../com/alarmy/near/presentation/feature/home/HomeScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 18f5d0d4..c0f4417d 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -163,7 +163,7 @@ internal fun HomeScreen( } } - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.weight(1f)) Text( text = stringResource(R.string.home_this_month_people), modifier = Modifier.padding(horizontal = 24.dp), @@ -314,7 +314,7 @@ private fun MyFriends( Box( modifier = Modifier - .fillMaxSize() + .fillMaxWidth().height(490.dp) .background( color = NearTheme.colors.WHITE_FFFFFF, shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), @@ -431,7 +431,7 @@ internal fun HomeScreenPreview() { HomeUiState( myFriendUIState = MyFriendUIState.Success( - List(5) { + List(7) { FriendSummary( id = "2003", name = "일이삼사오육칠팔구", From f50748bdacc1e861d9fbc973497199d7a5231a44 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 20:13:39 +0900 Subject: [PATCH 49/95] =?UTF-8?q?fix:=20My=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=B2=84=ED=8A=BC=20=ED=8C=A8=EB=94=A9=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 --- .../feature/myprofile/MyProfileScreen.kt | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/myprofile/MyProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/myprofile/MyProfileScreen.kt index 37f0092f..f62349e1 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/myprofile/MyProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/myprofile/MyProfileScreen.kt @@ -9,8 +9,11 @@ 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.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text @@ -129,6 +132,7 @@ fun MyProfileScreen( onWithdraw: () -> Unit = {}, onTermsClick: (TermsType) -> Unit = {}, ) { + val scrollState = rememberScrollState() NearFrame { // 앱바 NearTopAppbar( @@ -137,17 +141,21 @@ fun MyProfileScreen( onClickBackButton = onNavigateBack, ) - MyProfileInfoSection(uiState) - - // 일반 정보 섹션 Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), + modifier = Modifier.verticalScroll(scrollState), ) { - MyProfileGeneralSection(uiState) - MyProfileServiceInfoSection(onLogout, onWithdraw, onTermsClick) + MyProfileInfoSection(uiState) + + // 일반 정보 섹션 + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + ) { + MyProfileGeneralSection(uiState) + MyProfileServiceInfoSection(onLogout, onWithdraw, onTermsClick) + } } } } @@ -255,7 +263,7 @@ private fun ColumnScope.MyProfileServiceInfoSection( ) } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(63.dp)) NearLogoutButton( modifier = Modifier.fillMaxWidth(), From d6cece22f8fc3f35819901ce6a7d7b563201de86 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 20:18:46 +0900 Subject: [PATCH 50/95] =?UTF-8?q?fix(NearOutlinedTextField):=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EC=8B=9C=20container=20=EC=83=89=EC=83=81=EC=9D=B4=20?= =?UTF-8?q?=EB=B3=80=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/component/textfield/NearOutlinedTextField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/NearOutlinedTextField.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/NearOutlinedTextField.kt index 4b8e70a2..f159c151 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/NearOutlinedTextField.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/NearOutlinedTextField.kt @@ -151,7 +151,7 @@ fun NearOutlinedTextField( container = { OutlinedTextFieldDefaults.Container( enabled = enabled, - isError = isError, + isError = false, interactionSource = interactionSource, colors = colors, shape = shape, From 1e1b2e9df347dcdcdc06605bc69bda193301abe6 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:01:24 +0900 Subject: [PATCH 51/95] =?UTF-8?q?fix(Friend):=20DateTime=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EB=A1=9C=EC=A7=81=EC=9D=84=20Data=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EC=97=90=EC=84=9C=20=EC=88=98=ED=96=89?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - preview에서 DateTime 계산으로 렌더링이 안되는 문제 개선 --- .../alarmy/near/data/mapper/FriendMapper.kt | 20 +++++++++++++++++++ .../main/java/com/alarmy/near/model/Friend.kt | 14 +++---------- .../friendprofile/FriendProfileScreen.kt | 16 ++------------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt index e797a6ae..940f6ff0 100644 --- a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt +++ b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt @@ -13,6 +13,9 @@ import com.alarmy.near.network.response.AnniversaryEntity import com.alarmy.near.network.response.ContactFrequencyEntity import com.alarmy.near.network.response.FriendEntity import com.alarmy.near.utils.logger.NearLog +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Locale fun FriendEntity.toModel(): Friend = Friend( @@ -25,9 +28,26 @@ fun FriendEntity.toModel(): Friend = anniversaryList = anniversaryList.map { it.toModel() }, memo = memo, phone = phone, + isContactToday = lastContactAt?.isToday() ?: false, lastContactAt = lastContactAt, + lastContactFormat = lastContactAt?.contactFormat() ) +fun String.contactFormat(): String { + val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val outputFormatter = DateTimeFormatter.ofPattern("M월 d일") + + val date = LocalDate.parse(this, inputFormatter) + return date.format(outputFormatter) +} + +private fun String.isToday(): Boolean { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.KOREA) + val targetDate = LocalDate.parse(this, formatter) + val today = LocalDate.now() + return targetDate == today +} + fun ContactFrequencyEntity.toModel(): ContactFrequency = ContactFrequency( reminderInterval = contactWeek.toReminderInterval(), diff --git a/Near/app/src/main/java/com/alarmy/near/model/Friend.kt b/Near/app/src/main/java/com/alarmy/near/model/Friend.kt index b37b23a6..ba1cfcf3 100644 --- a/Near/app/src/main/java/com/alarmy/near/model/Friend.kt +++ b/Near/app/src/main/java/com/alarmy/near/model/Friend.kt @@ -22,17 +22,9 @@ data class Friend( val memo: String?, val phone: String?, val lastContactAt: String?, // "2025-07-16" -) : Parcelable { - val isContactedToday: Boolean - get() = lastContactAt?.isToday() ?: false - - private fun String.isToday(): Boolean { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.KOREA) - val targetDate = LocalDate.parse(this, formatter) - val today = LocalDate.now() - return targetDate == today - } -} + val lastContactFormat: String? = null, // 친구 상세에 쓰이는 포맷 TODO 추후 UI 모델 분리 + val isContactToday: Boolean? = null, // 친구 상세에 쓰이는 포맷 TODO 추후 UI 모델 분리 +) : Parcelable @Serializable @Parcelize diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt index fc6fb64c..abe45613 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt @@ -22,9 +22,7 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Surface @@ -75,8 +73,6 @@ import com.alarmy.near.presentation.ui.extension.onNoRippleClick import com.alarmy.near.presentation.ui.theme.NearTheme import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.time.LocalDate -import java.time.format.DateTimeFormatter @Composable fun FriendProfileRoute( @@ -268,7 +264,7 @@ fun FriendProfileScreen( text = stringResource( R.string.friend_profile_last_contact_date_format, - friend.lastContactAt.lastContactFormat(), + friend.lastContactFormat ?: "", ), style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.BLUE01_5AA2E9, @@ -391,7 +387,7 @@ fun FriendProfileScreen( .padding(bottom = 24.dp) .align(Alignment.BottomCenter), contentPadding = PaddingValues(vertical = 17.dp), - enabled = friend.isContactedToday.not(), + enabled = friend.isContactToday?.not() ?: false, onClick = { onRecordFriendShip(friend.friendId) }, text = stringResource(R.string.friend_profile_record_button_text), ) @@ -665,14 +661,6 @@ fun Modifier.customTabIndicatorOffset( .width(currentTabWidth) } -private fun String.lastContactFormat(): String { - val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - val outputFormatter = DateTimeFormatter.ofPattern("M월 d일") - - val date = LocalDate.parse(this, inputFormatter) - return date.format(outputFormatter) -} - @Preview(showBackground = true) @Composable fun FriendProfileScreenPreview() { From 18745a39f657c41bb7c1de477c00c0e96827ee5d Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:23:53 +0900 Subject: [PATCH 52/95] =?UTF-8?q?fix:=20FriendProfile=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EB=B2=94=EC=9C=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friendprofile/FriendProfileScreen.kt | 502 ++++++++++-------- 1 file changed, 288 insertions(+), 214 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt index abe45613..3e280e05 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.shape.RoundedCornerShape @@ -49,6 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -138,54 +140,55 @@ fun FriendProfileScreen( ) { val currentTabPosition = remember { mutableIntStateOf(0) } val dropdownState = remember { mutableStateOf(false) } -// val scrollState = rememberScrollState() NearFrame(modifier = modifier) { + if (recordSuccessDialogState) { + LaunchedEffect(true) { + if (recordSuccessDialogState) { + delay(2000L) + onDismissRecordSuccessDialog() + } + } + Dialog(onDismissRequest = onDismissRecordSuccessDialog) { + Column( + modifier = + Modifier + .width(255.dp) + .height(186.dp) + .background( + color = NearTheme.colors.WHITE_FFFFFF, + shape = RoundedCornerShape(16.dp), + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painterResource(R.drawable.img_100_character_success), + contentDescription = "", + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + stringResource(R.string.friend_profile_info_contact_success_text), + style = NearTheme.typography.B1_16_BOLD, + color = Color(0xff222222), + ) + } + } + } Box { when (friendState) { is FriendState.Success -> { val friend = friendState.friend + + // 전체를 세로로, 앱바는 고정, 나머지는 weight(1f) 로 영역 제한 Column( modifier = Modifier .align(Alignment.TopStart) .fillMaxSize() -// .verticalScroll(scrollState) .background(NearTheme.colors.WHITE_FFFFFF), ) { - if (recordSuccessDialogState) { - LaunchedEffect(true) { - if (recordSuccessDialogState) { - delay(2000L) - onDismissRecordSuccessDialog() - } - } - Dialog(onDismissRequest = onDismissRecordSuccessDialog) { - Column( - modifier = - Modifier - .width(255.dp) - .height(186.dp) - .background( - color = NearTheme.colors.WHITE_FFFFFF, - shape = RoundedCornerShape(16.dp), - ), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - painterResource(R.drawable.img_100_character_success), - contentDescription = "", - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - stringResource(R.string.friend_profile_info_contact_success_text), - style = NearTheme.typography.B1_16_BOLD, - color = Color(0xff222222), - ) - } - } - } + // (1) 상단 AppBar — 고정 NearTopAppbar( title = stringResource(R.string.friend_profile_title), onClickBackButton = onClickBackButton, @@ -193,10 +196,9 @@ fun FriendProfileScreen( Column(modifier = Modifier.padding(end = 20.dp)) { Image( modifier = - Modifier - .onNoRippleClick(onClick = { - dropdownState.value = true - }), + Modifier.onNoRippleClick(onClick = { + dropdownState.value = true + }), painter = painterResource(R.drawable.ic_32_menu), contentDescription = stringResource(R.string.common_menu_button_description), ) @@ -223,174 +225,209 @@ fun FriendProfileScreen( }, ) - Spacer(modifier = Modifier.height(18.dp)) - Row( + // (2) 나머지 영역을 제한된 높이로 만들기 -> 이 안에서 스크롤 가능 + Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 32.dp), - verticalAlignment = Alignment.CenterVertically, + .weight(1f), // <--- 요게 중요: 여기서 높이가 제한되어 내부 스크롤이 가능해짐 ) { - Box( - modifier = - Modifier, + // 컨텐츠는 스크롤 가능하거나 Lazy로 구성 + // 여기서는 상단의 프로필 요약(이미지/이름/버튼 등)을 스크롤 헤더로 포함시키고 + // 탭에 따라 ProfileTab(스크롤 필요 없음) 또는 RecordTab(LazyVerticalGrid)를 표시 + // 상단 요약을 포함한 전체를 LazyColumn으로 만들면 header + list 형태로 자연스럽게 동작 + + LazyColumn( + modifier = Modifier.fillMaxSize(), ) { - Image( - modifier = Modifier.align(Alignment.Center), - painter = painterResource(R.drawable.img_80_user1), - contentDescription = null, - ) - Image( - modifier = - Modifier - .align(Alignment.TopEnd) - .offset(x = 2.dp, y = (-2).dp), - painter = painterResource(R.drawable.ic_visual_24_emoji_0), - contentDescription = null, - ) - } - Spacer(modifier = Modifier.width(24.dp)) - Column { - Text( - modifier = Modifier.widthIn(max = 145.dp), - text = friend.name, - style = NearTheme.typography.B1_16_BOLD, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - if (friend.lastContactAt != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = - stringResource( - R.string.friend_profile_last_contact_date_format, - friend.lastContactFormat ?: "", - ), - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.BLUE01_5AA2E9, + item { + Spacer(modifier = Modifier.height(18.dp)) + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box { + // 이미지 + 이모지 + Image( + modifier = Modifier.align(Alignment.Center), + painter = painterResource(R.drawable.img_80_user1), + contentDescription = null, + ) + Image( + modifier = + Modifier + .align(Alignment.TopEnd) + .offset(x = 2.dp, y = (-2).dp), + painter = painterResource(R.drawable.ic_visual_24_emoji_0), + contentDescription = null, + ) + } + Spacer(modifier = Modifier.width(24.dp)) + Column { + Text( + modifier = Modifier.widthIn(max = 145.dp), + text = friend.name, + style = NearTheme.typography.B1_16_BOLD, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + if (friend.lastContactAt != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = + stringResource( + R.string.friend_profile_last_contact_date_format, + friend.lastContactFormat ?: "", + ), + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.BLUE01_5AA2E9, + ) + } + } + } + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) { + CallButton( + modifier = Modifier.weight(1f), + enabled = !friend.phone.isNullOrBlank(), + onClick = { + friend.phone?.let { + onClickCallButton(friend.phone) + } + }, + ) + Spacer(modifier = Modifier.width(7.dp)) + MessageButton( + Modifier.weight(1f), + enabled = !friend.phone.isNullOrBlank(), + onClick = { + friend.phone?.let { + onClickMessageButton(friend.phone) + } + }, + ) + } + Spacer(modifier = Modifier.height(24.dp)) + // TabRow + TabRow( + modifier = + Modifier + .padding(horizontal = 25.dp) + .width(170.dp), + containerColor = NearTheme.colors.WHITE_FFFFFF, + selectedTabIndex = currentTabPosition.intValue, + divider = {}, + indicator = { + TabRowDefaults.SecondaryIndicator( + modifier = + Modifier.customTabIndicatorOffset( + it[currentTabPosition.intValue], + 80.dp, + ), + height = 3.dp, + color = NearTheme.colors.BLUE01_5AA2E9, + ) + }, + ) { + Tab( + modifier = + Modifier + .width(85.dp) + .height(50.dp), + selected = currentTabPosition.intValue == 0, + onClick = { currentTabPosition.intValue = 0 }, + ) { + Text( + text = stringResource(R.string.friend_profile_tab_text_profile), + style = + if (currentTabPosition.intValue == + 0 + ) { + NearTheme.typography.B2_14_BOLD + } else { + NearTheme.typography.B2_14_MEDIUM + }, + color = + if (currentTabPosition.intValue == + 0 + ) { + NearTheme.colors.BLACK_1A1A1A + } else { + NearTheme.colors.GRAY02_B7B7B7 + }, + ) + } + Tab( + modifier = + Modifier + .width(85.dp) + .height(50.dp), + selected = currentTabPosition.intValue == 1, + onClick = { currentTabPosition.intValue = 1 }, + ) { + Text( + text = stringResource(R.string.friend_profile_tab_text_record), + style = + if (currentTabPosition.intValue == + 1 + ) { + NearTheme.typography.B2_14_BOLD + } else { + NearTheme.typography.B2_14_MEDIUM + }, + color = + if (currentTabPosition.intValue == + 1 + ) { + NearTheme.colors.BLACK_1A1A1A + } else { + NearTheme.colors.GRAY02_B7B7B7 + }, + ) + } + } + HorizontalDivider( + thickness = 1.dp, + color = NearTheme.colors.GRAY03_EBEBEB, ) + } // end header item + + // 탭별 본문을 lazy column의 item으로 넣음 — 이렇게 하면 중첩 스크롤 문제 회피 + item { + if (currentTabPosition.intValue == 0) { + // ProfileTab : 내부에 스크롤을 또 만들 필요 없음. LazyColumn이 전체를 스크롤함. + ProfileTab(friend = friend) + } else { + RecordTab( + friendShipRecordState = friendShipRecordState, + ) + } } - } - } - Spacer(modifier = Modifier.height(24.dp)) - Row( + item { Spacer(modifier = Modifier.height(80.dp)) } // 하단 여유 + } // end LazyColumn + } // end weighted Column + + // (3) 하단 고정 버튼 + NearSolidTypeButton( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp), - ) { - CallButton( - modifier = Modifier.weight(1f), - enabled = !friend.phone.isNullOrBlank(), - onClick = { - friend.phone?.let { - onClickCallButton(friend.phone) - } - }, - ) - Spacer(modifier = Modifier.width(7.dp)) - MessageButton( - Modifier.weight(1f), - enabled = !friend.phone.isNullOrBlank(), - onClick = { - friend.phone?.let { - onClickMessageButton(friend.phone) - } - }, - ) - } - Spacer(modifier = Modifier.height(24.dp)) - TabRow( - modifier = - Modifier - .padding(horizontal = 25.dp) - .width(170.dp), - containerColor = NearTheme.colors.WHITE_FFFFFF, - selectedTabIndex = 0, - divider = {}, - indicator = { - TabRowDefaults.SecondaryIndicator( - modifier = - Modifier - .customTabIndicatorOffset( - it[currentTabPosition.intValue], - 80.dp, - ), // 넓이, 애니메이션 지정 - // 모양 지정 - height = 3.dp, - color = NearTheme.colors.BLUE01_5AA2E9, - ) - }, - ) { - Tab( - modifier = - Modifier - .width(85.dp) - .height(50.dp), - selected = true, - onClick = { - currentTabPosition.intValue = 0 - }, - ) { - if (currentTabPosition.intValue == 0) { - Text( - text = stringResource(R.string.friend_profile_tab_text_profile), - style = NearTheme.typography.B2_14_BOLD, - color = NearTheme.colors.BLACK_1A1A1A, - ) - } else { - Text( - text = stringResource(R.string.friend_profile_tab_text_profile), - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.GRAY02_B7B7B7, - ) - } - } - Tab( - modifier = - Modifier - .width(85.dp) - .height(50.dp), - selected = true, - onClick = { - currentTabPosition.intValue = 1 - }, - ) { - if (currentTabPosition.intValue == 1) { - Text( - text = stringResource(R.string.friend_profile_tab_text_record), - style = NearTheme.typography.B2_14_BOLD, - color = NearTheme.colors.BLACK_1A1A1A, - ) - } else { - Text( - text = stringResource(R.string.friend_profile_tab_text_record), - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.GRAY02_B7B7B7, - ) - } - } - } - HorizontalDivider(thickness = 1.dp, color = NearTheme.colors.GRAY03_EBEBEB) - if (currentTabPosition.intValue == 0) { - ProfileTab(friend = friend) - } else { - RecordTab(friendShipRecordState = friendShipRecordState) - } - Spacer(modifier = Modifier.height(60.dp)) - } - NearSolidTypeButton( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .padding(bottom = 24.dp) - .align(Alignment.BottomCenter), - contentPadding = PaddingValues(vertical = 17.dp), - enabled = friend.isContactToday?.not() ?: false, - onClick = { onRecordFriendShip(friend.friendId) }, - text = stringResource(R.string.friend_profile_record_button_text), - ) + .padding(horizontal = 20.dp) + .padding(bottom = 24.dp), + contentPadding = PaddingValues(vertical = 17.dp), + enabled = friend.isContactToday?.not() ?: false, + onClick = { onRecordFriendShip(friend.friendId) }, + text = stringResource(R.string.friend_profile_record_button_text), + ) + } // end parent Column } is FriendState.Loading -> { @@ -407,9 +444,9 @@ fun FriendProfileScreen( ) } } - } - } - } + } // end when + } // end Box + } // end NearFrame } @Composable @@ -454,16 +491,28 @@ private fun RecordTab( modifier: Modifier = Modifier, friendShipRecordState: FriendShipRecordState, ) { - Column(modifier = modifier.padding(horizontal = 24.dp)) { + Column( + modifier = + modifier + .fillMaxWidth() // 부모 스크롤(LazyColumn)에 의존하도록 fillMaxWidth만 사용 + .padding(horizontal = 24.dp), + ) { Spacer(modifier = Modifier.height(24.dp)) Text( stringResource(R.string.friend_profile_info_record_title_text), style = NearTheme.typography.B2_14_BOLD, color = NearTheme.colors.BLACK_1A1A1A, ) + if (friendShipRecordState.isEmpty) { - Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { - Spacer(modifier = Modifier.height(60.dp)) + // 빈 상태는 중앙 정렬이지만 높이를 무한으로 잡지 않도록 fillMaxWidth 사용 + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 60.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { Image(painterResource(R.drawable.img_100_character_empty), contentDescription = null) Spacer(modifier = Modifier.height(16.dp)) Text( @@ -474,19 +523,44 @@ private fun RecordTab( } } else { Spacer(modifier = Modifier.height(13.dp)) - LazyVerticalGrid( - columns = GridCells.Fixed(3), - verticalArrangement = Arrangement.spacedBy(24.dp), - contentPadding = PaddingValues(bottom = 60.dp), - ) { - items(friendShipRecordState.records.size) { - RecordItem( - friendRecord = friendShipRecordState.records[friendShipRecordState.records.size - 1 - it], - index = - friendShipRecordState.records.size - it, - ) + + // records를 3개씩 묶어서 행(Row)으로 렌더 — 자체 스크롤 없음 + val rows = friendShipRecordState.records.chunked(3) + Column(modifier = Modifier.fillMaxWidth()) { + rows.forEach { rowItems -> + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + // 아이템 간 세로 간격 + horizontalArrangement = Arrangement.spacedBy(24.dp), + ) { + // 각 row의 아이템을 동일한 폭으로 나눔 + rowItems.forEachIndexed { index, item -> + // 인덱스나 번호 표시 로직은 기존과 동일하게 계산 + val globalIndex = + friendShipRecordState.records.size - (rows.indexOf(rowItems) * 3 + index) + Box(modifier = Modifier.weight(1f)) { + RecordItem( + modifier = Modifier.wrapContentSize(), + index = globalIndex, + friendRecord = item, + ) + } + } + + // 만약 마지막 행에 3 미만의 아이템이 있으면 빈 공간을 채워 균형 맞춤 + if (rowItems.size < 3) { + repeat(3 - rowItems.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } } } + + Spacer(modifier = Modifier.height(24.dp)) } } } From 59c9ebc88301bdf4eac22d42537d685057753d47 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:34:41 +0900 Subject: [PATCH 53/95] =?UTF-8?q?feat:=20=EC=B1=99=EA=B9=80=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=ED=95=98=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EA=B7=B8?= =?UTF-8?q?=EB=9D=BC=EB=8D=B0=EC=9D=B4=EC=85=98=20=EB=B0=B0=EA=B2=BD=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friendprofile/FriendProfileScreen.kt | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt index 3e280e05..22af9929 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt @@ -41,6 +41,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.res.painterResource @@ -416,17 +418,29 @@ fun FriendProfileScreen( } // end weighted Column // (3) 하단 고정 버튼 - NearSolidTypeButton( + Box( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .padding(bottom = 24.dp), - contentPadding = PaddingValues(vertical = 17.dp), - enabled = friend.isContactToday?.not() ?: false, - onClick = { onRecordFriendShip(friend.friendId) }, - text = stringResource(R.string.friend_profile_record_button_text), - ) + Modifier.fillMaxWidth().background( + brush = + Brush.linearGradient( + colors = listOf(Color(0x00FFFFFF), Color(0xFFFFFFFF)), // 파랑 → 밝은 하늘색 + start = Offset(0f, 0f), // 위쪽 시작 + end = Offset(0f, Float.POSITIVE_INFINITY) + ), + ), + ) { + NearSolidTypeButton( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(bottom = 24.dp, top = 16.dp), + contentPadding = PaddingValues(vertical = 17.dp), + enabled = friend.isContactToday?.not() ?: false, + onClick = { onRecordFriendShip(friend.friendId) }, + text = stringResource(R.string.friend_profile_record_button_text), + ) + } } // end parent Column } From 9b28065f2faeb5dcb3acdae67c6cd13a3b9a34d9 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:36:45 +0900 Subject: [PATCH 54/95] =?UTF-8?q?fix:=20=EC=B9=9C=EA=B5=AC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20=EB=A9=94=EB=AA=A8?= =?UTF-8?q?=EC=99=80=20BasicText=EA=B0=84=20=EA=B0=84=EA=B2=A9=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 --- .../feature/friendprofileedittor/FriendProfileEditorScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt index 7729f3f8..79c37e95 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt @@ -574,7 +574,7 @@ fun FriendProfileEditorScreen( style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.GRAY01_888888, ) - Spacer(modifier = Modifier.width(23.dp)) + Spacer(modifier = Modifier.width(62.dp)) NearLimitedTextField( modifier = Modifier From 7edaef96869200fe1c07501cafe7e29ff936a7ca Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:44:12 +0900 Subject: [PATCH 55/95] =?UTF-8?q?fix:=20UI=20=ED=91=9C=EC=8B=9C=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=82=A0=EC=A7=9C=20=ED=8F=AC=EB=A7=B7=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 --- .../presentation/feature/friendprofile/FriendProfileScreen.kt | 2 +- .../friendprofileedittor/uistate/FriendProfileEditorUIState.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt index 22af9929..16d2a3a3 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt @@ -489,7 +489,7 @@ private fun ProfileTab( category = stringResource(R.string.friend_profile_info_category_anniversary), content = friend.anniversaryList.joinToString(" ") { - "${it.title} (${it.date})" + "${it.title} (${it.date?.replace("-",".")})" }, ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt index 4e92060a..ea436d3f 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt @@ -52,7 +52,7 @@ fun FriendProfileEditorUIState.toModel( relation = relation, contactFrequency = contactFrequency, - birthday = birthday.value?.replace(".", "-"), + birthday = birthday.value?.replace(".", "."), anniversaryList = anniversaries.map { Anniversary( From dd873cbb6917b4d5c02461787e912c511c895253 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:45:56 +0900 Subject: [PATCH 56/95] =?UTF-8?q?fix:=20=EC=A0=9C=ED=95=9C=20=EA=B8=80?= =?UTF-8?q?=EC=9E=90=20=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friendprofileedittor/FriendProfileEditorViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt index 8892075e..10be95b3 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt @@ -202,6 +202,6 @@ class FriendProfileEditorViewModel companion object { private const val MAX_NAME_LENGTH = 20 - private const val MAX_MEMO_LENGTH = 200 + private const val MAX_MEMO_LENGTH = 100 } } From cfe415c59eb5a1fe251346756a0cc31440ea8d02 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:46:12 +0900 Subject: [PATCH 57/95] =?UTF-8?q?fix:=20=ED=99=88=20lazyRow=20=ED=82=A4=20?= =?UTF-8?q?=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/alarmy/near/presentation/feature/home/HomeScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index c0f4417d..b30c3da1 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -217,7 +217,7 @@ internal fun HomeScreen( items( count = monthlyFriends.size, key = { - monthlyFriends[it].friendId + "monthly_friend_${monthlyFriends[it].friendId}" }, ) { val monthlyContact = monthlyFriends[it] From 477cf7ad60d82dd320eef723c47af2b918372f9f Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Wed, 12 Nov 2025 18:39:00 +0900 Subject: [PATCH 58/95] =?UTF-8?q?fix:=20=EC=B9=9C=EA=B5=AC=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=EC=8B=9C=20=EC=83=9D=EC=9D=BC=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt | 4 ++-- .../uistate/FriendProfileEditorUIState.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt index 940f6ff0..b11dc98f 100644 --- a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt +++ b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt @@ -30,7 +30,7 @@ fun FriendEntity.toModel(): Friend = phone = phone, isContactToday = lastContactAt?.isToday() ?: false, lastContactAt = lastContactAt, - lastContactFormat = lastContactAt?.contactFormat() + lastContactFormat = lastContactAt?.contactFormat(), ) fun String.contactFormat(): String { @@ -66,7 +66,7 @@ fun Friend.toRequest(): FriendRequest = name = name, relation = relation.toString(), contactFrequency = contactFrequency.toRequest(), - birthday = birthday, + birthday = birthday?.replace(".", "-"), anniversaryList = anniversaryList.map { it.toRequest() }, memo = memo, phone = phone, diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt index ea436d3f..45b423ce 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt @@ -52,7 +52,7 @@ fun FriendProfileEditorUIState.toModel( relation = relation, contactFrequency = contactFrequency, - birthday = birthday.value?.replace(".", "."), + birthday = birthday.value?.replace("-", "."), anniversaryList = anniversaries.map { Anniversary( From ffda528ae2622b11875ffa6a2bcf21e838ecb32d Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 13 Nov 2025 00:01:34 +0900 Subject: [PATCH 59/95] =?UTF-8?q?fix:=20=EC=B9=9C=EA=B5=AC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=8B=9C=20=EC=9D=B4=EB=B2=88=EB=8B=AC=20=EC=B1=99?= =?UTF-8?q?=EA=B8=B8=20=EC=82=AC=EB=9E=8C=EB=8F=84=20=EC=A0=9C=EC=99=B8?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../near/presentation/feature/home/HomeViewModel.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt index c52e39c8..96b35ec4 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt @@ -9,7 +9,6 @@ import com.alarmy.near.presentation.feature.home.model.MonthlyFriendUIState import com.alarmy.near.presentation.feature.home.model.MyFriendUIState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch @@ -68,10 +67,14 @@ class HomeViewModel } } launch { - friendRepository - .fetchMonthlyFriends() - .catch { - _errorEvent.send(it) + combine( + friendRepository + .fetchMonthlyFriends(), + deletedFriendIdsFlow, + ) { monthlyFriends, deletedIds -> + monthlyFriends.filter { it.friendId !in deletedIds } + }.catch { + _errorEvent.send(it) }.collect { _uiState.update { state -> state.copy( From 6cc3111f2f0c4d06fa75a142705bf52ba338713b Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 13 Nov 2025 00:15:54 +0900 Subject: [PATCH 60/95] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A6=AC=ED=8C=85=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=A0=95=EB=A0=AC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/feature/home/HomeScreen.kt | 102 +++++++++--------- 1 file changed, 52 insertions(+), 50 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index b30c3da1..43e06e07 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -118,52 +119,50 @@ internal fun HomeScreen( R.drawable.img_bg, ), contentScale = ContentScale.FillBounds, - ) - .fillMaxSize(), + ).fillMaxSize(), ) { Spacer(modifier = Modifier.height(statusBarHeightDp)) - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - .padding(end = 20.dp), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.onNoRippleClick(onClick = onMyPageClick), - text = stringResource(R.string.home_my_profile_button_text), - style = NearTheme.typography.H2_18_BOLD.copy(letterSpacing = 0.sp), - color = NearTheme.colors.WHITE_FFFFFF, - ) - Spacer(modifier = Modifier.width(12.dp)) - Image( - modifier = Modifier.onNoRippleClick(onClick = onAlarmClick), - painter = painterResource(R.drawable.ic_32_bell), - contentDescription = "", - ) - } - Spacer(modifier = Modifier.height(16.dp)) - - Box(modifier = Modifier.padding(horizontal = 24.dp)) { - NearConditionalShimmer(enabled = uiState.memberInfo == null) { + Box(modifier = Modifier.weight(1f)) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .padding(end = 20.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { Text( - text = - buildAnnotatedString { - append("${uiState.memberInfo?.nickname}님,\n") - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append("누구를 챙길지") - } - append(" 정해볼까요?") - }, - style = NearTheme.typography.H1_24_REGULAR, + modifier = Modifier.onNoRippleClick(onClick = onMyPageClick), + text = stringResource(R.string.home_my_profile_button_text), + style = NearTheme.typography.H2_18_BOLD.copy(letterSpacing = 0.sp), color = NearTheme.colors.WHITE_FFFFFF, ) + Spacer(modifier = Modifier.width(12.dp)) + Image( + modifier = Modifier.onNoRippleClick(onClick = onAlarmClick), + painter = painterResource(R.drawable.ic_32_bell), + contentDescription = "", + ) } - } - Spacer(modifier = Modifier.weight(1f)) + Box(modifier = Modifier.padding(horizontal = 24.dp).align(Alignment.CenterStart)) { + NearConditionalShimmer(enabled = uiState.memberInfo == null) { + Text( + text = + buildAnnotatedString { + append("${uiState.memberInfo?.nickname}님,\n") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append("누구를 챙길지") + } + append(" 정해볼까요?") + }, + style = NearTheme.typography.H1_24_REGULAR, + color = NearTheme.colors.WHITE_FFFFFF, + ) + } + } + } Text( text = stringResource(R.string.home_this_month_people), modifier = Modifier.padding(horizontal = 24.dp), @@ -314,17 +313,18 @@ private fun MyFriends( Box( modifier = Modifier - .fillMaxWidth().height(490.dp) + .fillMaxWidth() + .height(490.dp) .background( color = NearTheme.colors.WHITE_FFFFFF, shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), ), ) { if (myFriendUIState is MyFriendUIState.Loading) { - Column( modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally) { + horizontalAlignment = Alignment.CenterHorizontally, + ) { Spacer(modifier = Modifier.height(166.dp)) NearConditionalShimmer( shimmerColors = ShimmerType.WHITE.colors, @@ -443,14 +443,16 @@ internal fun HomeScreenPreview() { }, ), monthlyFriendUIState = - MonthlyFriendUIState.Success(List(4) { - MonthlyFriend( - friendId = "intellegat$it", - name = "Stacey Stewart", - type = MonthlyFriendType.ANNIVERSARY, - nextContactAt = "2025-09-30", - ) - },), + MonthlyFriendUIState.Success( + List(4) { + MonthlyFriend( + friendId = "intellegat$it", + name = "Stacey Stewart", + type = MonthlyFriendType.ANNIVERSARY, + nextContactAt = "2025-09-30", + ) + }, + ), memberInfo = MemberInfo( memberId = "posidonium", From 6657de626d887a2a32bddea4e7908d57650d36f3 Mon Sep 17 00:00:00 2001 From: stopstone Date: Thu, 13 Nov 2025 00:40:00 +0900 Subject: [PATCH 61/95] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9A=A9=20=EB=B0=94?= =?UTF-8?q?=ED=85=80=EC=8B=9C=ED=8A=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=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 - 재사용 가능한 `NearBottomSheet` 컴포posable을 구현했습니다. --- .../component/bottomsheet/NearBottomSheet.kt | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt new file mode 100644 index 00000000..b41ebf42 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt @@ -0,0 +1,52 @@ +package com.alarmy.near.presentation.ui.component.bottomsheet + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.alarmy.near.presentation.ui.theme.NearTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NearBottomSheet( + isVisible: Boolean, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + if (isVisible) { + val bottomSheetState = + rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = bottomSheetState, + dragHandle = { + BottomSheetDefaults.DragHandle( + color = NearTheme.colors.BLACK_1A1A1A.copy(alpha = 0.1f), + width = 36.dp, + height = 5.dp, + ) + }, + modifier = modifier, + containerColor = NearTheme.colors.WHITE_FFFFFF, + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + ) { + content() + } + } + } +} From a376cf8fea9654fb82b2c6f15ce512344924205e Mon Sep 17 00:00:00 2001 From: stopstone Date: Thu, 13 Nov 2025 00:40:53 +0900 Subject: [PATCH 62/95] =?UTF-8?q?refactor:=20`CycleSettingBottomSheet`?= =?UTF-8?q?=EC=9D=84=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/CycleSettingBottomSheet.kt | 297 ++++++++---------- 1 file changed, 137 insertions(+), 160 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt index d09b78a6..9753eafb 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt @@ -2,19 +2,14 @@ package com.alarmy.near.presentation.feature.friendcontactcycle.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.VerticalDivider -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -31,12 +26,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.alarmy.near.R import com.alarmy.near.model.ReminderInterval +import com.alarmy.near.presentation.ui.component.bottomsheet.NearBottomSheet import com.alarmy.near.presentation.ui.component.checkbox.NearCheckbox import com.alarmy.near.presentation.ui.extension.onNoRippleClick import com.alarmy.near.presentation.ui.theme.NearTheme import com.alarmy.near.utils.extensions.DateExtension -@OptIn(ExperimentalMaterial3Api::class) @Composable fun CycleSettingBottomSheet( isVisible: Boolean, @@ -45,178 +40,160 @@ fun CycleSettingBottomSheet( currentSelectedInterval: ReminderInterval? = null, modifier: Modifier = Modifier, ) { - // 선택된 주기 상태 관리 (기존 선택값이 있으면 그것을 사용, 없으면 매주를 기본값으로) + // 선택된 주기 상태 관리 var selectedInterval by remember(isVisible) { mutableStateOf(currentSelectedInterval ?: ReminderInterval.EVERY_WEEK) } - if (isVisible) { - val bottomSheetState = - rememberModalBottomSheetState( - skipPartiallyExpanded = true, + NearBottomSheet( + isVisible = isVisible, + onDismiss = onDismiss, + modifier = modifier, + ) { + // 제목 + Text( + text = stringResource(R.string.friend_contact_cycle_cycle_setting_text), + style = NearTheme.typography.B1_16_BOLD, + ) + + Spacer(modifier = Modifier.size(24.dp)) + + // 선택된 주기 미리보기 + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(NearTheme.colors.BG02_F4F9FD) + .padding(vertical = 18.dp, horizontal = 20.dp), + ) { + Text( + text = + buildAnnotatedString { + val interval = selectedInterval ?: ReminderInterval.EVERY_WEEK + val cycleText = DateExtension.getCycleText(interval) + val firstSpaceIndex = cycleText.indexOf(' ') + + if (firstSpaceIndex != -1) { + withStyle( + style = + SpanStyle( + color = NearTheme.colors.BLACK_1A1A1A, + fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, + ), + ) { + append(cycleText.substring(0, firstSpaceIndex + 1)) + } + withStyle( + style = + SpanStyle( + color = NearTheme.colors.BLUE01_5AA2E9, + fontWeight = NearTheme.typography.B2_14_BOLD.fontWeight, + ), + ) { + append(cycleText.substring(firstSpaceIndex + 1)) + } + } else { + withStyle( + style = + SpanStyle( + color = NearTheme.colors.BLACK_1A1A1A, + fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, + ), + ) { + append(cycleText) + } + } + }, + style = NearTheme.typography.B2_14_MEDIUM, ) - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = bottomSheetState, - dragHandle = { - BottomSheetDefaults.DragHandle( - color = NearTheme.colors.BLACK_1A1A1A.copy(alpha = 0.1f), - width = 36.dp, - height = 5.dp, + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.size(20.dp)) + + VerticalDivider( + modifier = + Modifier + .size(width = 1.dp, height = 28.dp), + color = + NearTheme.colors.BLACK_1A1A1A.copy( + alpha = 0.1f, + ), ) - }, - modifier = modifier, - containerColor = NearTheme.colors.WHITE_FFFFFF, - ) { - Column( + + Spacer(modifier = Modifier.size(20.dp)) + + Text( + text = + stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + + ( + selectedInterval?.let { DateExtension.getNextCycleDate(it) } + ?: DateExtension.getNextWeekSameDay() + ), + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.GRAY01_888888, + ) + } + } + + Spacer(modifier = Modifier.size(8.dp)) + + // 주기 선택 옵션들 + ReminderInterval.entries.forEach { interval -> + val isSelected = selectedInterval == interval + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp), + .onNoRippleClick { + if (!isSelected) { + selectedInterval = interval + } + }.padding(vertical = 15.dp), ) { Text( - text = stringResource(R.string.friend_contact_cycle_cycle_setting_text), - style = NearTheme.typography.B1_16_BOLD, + text = stringResource(interval.labelRes), + style = + if (isSelected) { + NearTheme.typography.B1_16_BOLD + } else { + NearTheme.typography.B2_14_MEDIUM + }, ) - Spacer(modifier = Modifier.size(24.dp)) - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = - modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .background(NearTheme.colors.BG02_F4F9FD) - .padding(vertical = 18.dp, horizontal = 20.dp), - ) { - Text( - text = - buildAnnotatedString { - val interval = selectedInterval ?: ReminderInterval.EVERY_WEEK - val cycleText = DateExtension.getCycleText(interval) - val firstSpaceIndex = cycleText.indexOf(' ') - - if (firstSpaceIndex != -1) { - withStyle( - style = - SpanStyle( - color = NearTheme.colors.BLACK_1A1A1A, - fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, - ), - ) { - append(cycleText.substring(0, firstSpaceIndex + 1)) - } - withStyle( - style = - SpanStyle( - color = NearTheme.colors.BLUE01_5AA2E9, - fontWeight = NearTheme.typography.B2_14_BOLD.fontWeight, - ), - ) { - append(cycleText.substring(firstSpaceIndex + 1)) - } - } else { - withStyle( - style = - SpanStyle( - color = NearTheme.colors.BLACK_1A1A1A, - fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, - ), - ) { - append(cycleText) - } - } - }, - style = NearTheme.typography.B2_14_MEDIUM, + if (isSelected) { + NearCheckbox( + checked = true, + onCheckedChange = { checked -> + // 체크박스 클릭 시 해제되지 않도록 수정 + // 체크된 상태를 유지 + }, ) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = Modifier.size(20.dp)) - - VerticalDivider( - modifier = - Modifier - .size(width = 1.dp, height = 28.dp), - color = - NearTheme.colors.BLACK_1A1A1A.copy( - alpha = 0.1f, - ), - ) - - Spacer(modifier = Modifier.size(20.dp)) - - Text( - text = - stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + - ( - selectedInterval?.let { DateExtension.getNextCycleDate(it) } - ?: DateExtension.getNextWeekSameDay() - ), - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.GRAY01_888888, - ) - } } - - Spacer(modifier = Modifier.size(8.dp)) - - ReminderInterval.entries.forEach { interval -> - val isSelected = selectedInterval == interval - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = - modifier - .fillMaxWidth() - .onNoRippleClick { - if (!isSelected) { - selectedInterval = interval - } - }.padding(vertical = 15.dp), - ) { - Text( - text = stringResource(interval.labelRes), - style = - if (isSelected) { - NearTheme.typography.B1_16_BOLD - } else { - NearTheme.typography.B2_14_MEDIUM - }, - ) - - if (isSelected) { - NearCheckbox( - checked = true, - onCheckedChange = { checked -> - // 체크박스 클릭 시 해제되지 않도록 수정 - // 체크된 상태를 유지 - }, - ) - } - } - } - Spacer(modifier = Modifier.size(24.dp)) - - ContactCycleButtons( - onLeftButtonClick = onDismiss, - onRightButtonClick = { - selectedInterval?.let { interval -> - onComplete(interval) - onDismiss() - } - }, - leftButtonText = stringResource(R.string.friend_contact_cycle_cancel_button), - rightButtonText = stringResource(R.string.friend_contact_cycle_complete_button), - ) - Spacer(modifier = Modifier.size(24.dp)) } } + Spacer(modifier = Modifier.size(24.dp)) + + // 버튼들 + ContactCycleButtons( + onLeftButtonClick = onDismiss, + onRightButtonClick = { + selectedInterval?.let { interval -> + onComplete(interval) + onDismiss() + } + }, + leftButtonText = stringResource(R.string.friend_contact_cycle_cancel_button), + rightButtonText = stringResource(R.string.friend_contact_cycle_complete_button), + ) + Spacer(modifier = Modifier.size(24.dp)) } } From 576da8556b1a1c6fd13e6e787e239db0016ad0ae Mon Sep 17 00:00:00 2001 From: stopstone Date: Thu, 13 Nov 2025 00:48:12 +0900 Subject: [PATCH 63/95] =?UTF-8?q?refactor:=20=EC=A3=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20BottomSheet=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 `FriendProfileEditor` 화면에서만 사용하던 `ReminderIntervalBottomSheet`를 삭제했습니다. - 여러 화면에서 재사용 가능하도록 `CycleSettingBottomSheet`라는 이름의 공용 컴포넌트를 새로 만들었습니다. - 새로운 BottomSheet는 상태 관리를 개선하고, 선택된 주기에 따라 미리보기 UI가 동적으로 변경되도록 수정했습니다. - `FriendProfileEditorScreen`에서 기존 BottomSheet를 새로운 `CycleSettingBottomSheet`로 교체 적용했습니다. --- .../FriendProfileEditorScreen.kt | 18 +- .../component/CycleSettingBottomSheet.kt | 210 ++++++++++++++++++ .../component/ReminderIntervalBottomSheet.kt | 205 ----------------- 3 files changed, 220 insertions(+), 213 deletions(-) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt delete mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/ReminderIntervalBottomSheet.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt index 7729f3f8..af922137 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt @@ -44,8 +44,8 @@ import com.alarmy.near.model.DayOfWeek import com.alarmy.near.model.Friend import com.alarmy.near.model.Relation import com.alarmy.near.model.ReminderInterval +import com.alarmy.near.presentation.feature.friendcontactcycle.components.CycleSettingBottomSheet import com.alarmy.near.presentation.feature.friendprofileedittor.component.NearDatePicker -import com.alarmy.near.presentation.feature.friendprofileedittor.component.ReminderIntervalBottomSheet import com.alarmy.near.presentation.feature.friendprofileedittor.dialog.EditorExitDialog import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.FriendProfileEditorUIEvent import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.FriendProfileEditorUIState @@ -140,14 +140,16 @@ fun FriendProfileEditorScreen( onCloseDialog: () -> Unit = {}, ) { val showBottomSheet = remember { mutableStateOf(false) } - if (showBottomSheet.value) { - ReminderIntervalBottomSheet(onDismissRequest = { + CycleSettingBottomSheet( + isVisible = showBottomSheet.value, + onDismiss = { showBottomSheet.value = false - }, onSelectReminderInterval = { - onReminderIntervalChanged(it) - showBottomSheet.value = false - }) - } + }, + onComplete = { selectedInterval -> + onReminderIntervalChanged(selectedInterval) + }, + currentSelectedInterval = friendProfileEditorUIState.contactFrequency.reminderInterval, + ) if (dialogState) { EditorExitDialog( onDismissRequest = { diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt new file mode 100644 index 00000000..26212231 --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt @@ -0,0 +1,210 @@ +package com.alarmy.near.presentation.feature.friendprofileedittor.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.alarmy.near.R +import com.alarmy.near.model.ReminderInterval +import com.alarmy.near.presentation.feature.friendcontactcycle.components.ContactCycleButtons +import com.alarmy.near.presentation.ui.component.bottomsheet.NearBottomSheet +import com.alarmy.near.presentation.ui.component.checkbox.NearCheckbox +import com.alarmy.near.presentation.ui.extension.onNoRippleClick +import com.alarmy.near.presentation.ui.theme.NearTheme +import com.alarmy.near.utils.extensions.DateExtension + +@Composable +fun CycleSettingBottomSheet( + isVisible: Boolean, + onDismiss: () -> Unit, + onComplete: (ReminderInterval) -> Unit = {}, + currentSelectedInterval: ReminderInterval? = null, + modifier: Modifier = Modifier, +) { + // 선택된 주기 상태 관리 + var selectedInterval by remember(isVisible) { + mutableStateOf(currentSelectedInterval ?: ReminderInterval.EVERY_WEEK) + } + + NearBottomSheet( + isVisible = isVisible, + onDismiss = onDismiss, + modifier = modifier, + ) { + // 제목 + Text( + text = stringResource(R.string.friend_contact_cycle_cycle_setting_text), + style = NearTheme.typography.B1_16_BOLD, + ) + + Spacer(modifier = Modifier.size(24.dp)) + + // 선택된 주기 미리보기 + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(NearTheme.colors.BG02_F4F9FD) + .padding(vertical = 18.dp, horizontal = 20.dp), + ) { + Text( + text = + buildAnnotatedString { + val interval = selectedInterval ?: ReminderInterval.EVERY_WEEK + val cycleText = DateExtension.getCycleText(interval) + val firstSpaceIndex = cycleText.indexOf(' ') + + if (firstSpaceIndex != -1) { + withStyle( + style = + SpanStyle( + color = NearTheme.colors.BLACK_1A1A1A, + fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, + ), + ) { + append(cycleText.substring(0, firstSpaceIndex + 1)) + } + withStyle( + style = + SpanStyle( + color = NearTheme.colors.BLUE01_5AA2E9, + fontWeight = NearTheme.typography.B2_14_BOLD.fontWeight, + ), + ) { + append(cycleText.substring(firstSpaceIndex + 1)) + } + } else { + withStyle( + style = + SpanStyle( + color = NearTheme.colors.BLACK_1A1A1A, + fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, + ), + ) { + append(cycleText) + } + } + }, + style = NearTheme.typography.B2_14_MEDIUM, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.size(20.dp)) + + VerticalDivider( + modifier = + Modifier + .size(width = 1.dp, height = 28.dp), + color = + NearTheme.colors.BLACK_1A1A1A.copy( + alpha = 0.1f, + ), + ) + + Spacer(modifier = Modifier.size(20.dp)) + + Text( + text = + stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + + ( + selectedInterval?.let { DateExtension.getNextCycleDate(it) } + ?: DateExtension.getNextWeekSameDay() + ), + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.GRAY01_888888, + ) + } + } + + Spacer(modifier = Modifier.size(8.dp)) + + // 주기 선택 옵션들 + ReminderInterval.entries.forEach { interval -> + val isSelected = selectedInterval == interval + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .onNoRippleClick { + if (!isSelected) { + selectedInterval = interval + } + }.padding(vertical = 15.dp), + ) { + Text( + text = stringResource(interval.labelRes), + style = + if (isSelected) { + NearTheme.typography.B1_16_BOLD + } else { + NearTheme.typography.B2_14_MEDIUM + }, + ) + + if (isSelected) { + NearCheckbox( + checked = true, + onCheckedChange = { checked -> + // 체크박스 클릭 시 해제되지 않도록 수정 + // 체크된 상태를 유지 + }, + ) + } + } + } + Spacer(modifier = Modifier.size(24.dp)) + + // 버튼들 + ContactCycleButtons( + onLeftButtonClick = onDismiss, + onRightButtonClick = { + selectedInterval?.let { interval -> + onComplete(interval) + onDismiss() + } + }, + leftButtonText = stringResource(R.string.friend_contact_cycle_cancel_button), + rightButtonText = stringResource(R.string.friend_contact_cycle_complete_button), + ) + Spacer(modifier = Modifier.size(24.dp)) + } +} + +@Preview +@Composable +fun CycleSettingBottomSheetPreview() { + NearTheme { + CycleSettingBottomSheet( + isVisible = true, + onDismiss = {}, + ) + } +} diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/ReminderIntervalBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/ReminderIntervalBottomSheet.kt deleted file mode 100644 index ce7902f1..00000000 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/ReminderIntervalBottomSheet.kt +++ /dev/null @@ -1,205 +0,0 @@ -package com.alarmy.near.presentation.feature.friendprofileedittor.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.VerticalDivider -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.material3.rememberStandardBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.alarmy.near.model.ReminderInterval -import com.alarmy.near.presentation.ui.component.button.NearLineTypeButton -import com.alarmy.near.presentation.ui.component.button.NearSolidTypeButton -import com.alarmy.near.presentation.ui.component.checkbox.NearCheckbox -import com.alarmy.near.presentation.ui.extension.onNoRippleClick -import com.alarmy.near.presentation.ui.theme.NearTheme - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ReminderIntervalBottomSheet( - modifier: Modifier = Modifier, - selectedReminderInterval: ReminderInterval = ReminderInterval.EVERY_WEEK, - onSelectReminderInterval: (ReminderInterval) -> Unit = {}, - sheetState: SheetState = - rememberModalBottomSheetState( - skipPartiallyExpanded = true, - ), - onDismissRequest: () -> Unit = {}, -) { - val initialReminderInterval = remember { selectedReminderInterval } - - val tempSelected = remember { mutableStateOf(initialReminderInterval) } - ModalBottomSheet( - modifier = modifier, - containerColor = NearTheme.colors.WHITE_FFFFFF, - sheetState = sheetState, - onDismissRequest = { - tempSelected.value = initialReminderInterval - onDismissRequest() - }, - dragHandle = { - Surface( - modifier = Modifier.padding(top = 12.dp, bottom = 24.dp), - color = Color.Black.copy(alpha = 0.1f), - ) { - Box( - modifier = - Modifier - .width(36.dp) - .height(5.dp), - ) - } - }, - ) { - Text( - modifier = Modifier.padding(start = 24.dp), - text = "주기 설정", - style = NearTheme.typography.B1_16_BOLD, - color = Color.Black, - ) - Spacer(modifier = Modifier.height(24.dp)) - Surface( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - color = NearTheme.colors.BG02_F4F9FD, - shape = RoundedCornerShape(12.dp), - ) { - Row( - modifier = Modifier.padding(horizontal = 20.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - buildAnnotatedString { - append("매주") - withStyle( - style = - SpanStyle( - color = NearTheme.colors.BLUE01_5AA2E9, - fontWeight = FontWeight.Bold, - ), - ) { - append(" 화요일") - } - }, - color = NearTheme.colors.BLACK_1A1A1A, - style = NearTheme.typography.B2_14_MEDIUM, - ) - Row(verticalAlignment = Alignment.CenterVertically) { - VerticalDivider( - thickness = 1.dp, - modifier = Modifier.height(28.dp), - color = NearTheme.colors.BLACK_1A1A1A.copy(0.1f), - ) - Spacer(modifier = Modifier.width(20.dp)) - Text( - text = "다음 주기 : 4/8 화", - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.GRAY01_888888, - ) - } - } - } - Spacer(modifier = Modifier.height(8.dp)) - LazyColumn( - verticalArrangement = Arrangement.spacedBy(28.dp), - contentPadding = PaddingValues(vertical = 14.dp), - ) { - items( - count = ReminderInterval.entries.size, - key = { index -> ReminderInterval.entries[index] }, - ) { item -> - val reminderInterval = ReminderInterval.entries[item] - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(start = 24.dp, end = 14.dp) - .onNoRippleClick(onClick = { - tempSelected.value = reminderInterval - }), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - stringResource(reminderInterval.labelRes), - style = - if (reminderInterval == tempSelected.value) { - NearTheme.typography.B1_16_BOLD - } else { - NearTheme.typography.B1_16_MEDIUM - }, - color = NearTheme.colors.BLACK_1A1A1A, - ) - NearCheckbox( - checked = tempSelected.value == reminderInterval, - onCheckedChange = {}, - ) - } - } - } - Spacer(modifier = Modifier.height(24.dp)) - Row(modifier = Modifier.padding(horizontal = 20.dp)) { - NearLineTypeButton( - modifier = Modifier.weight(1f), - text = "취소", - onClick = { - tempSelected.value = initialReminderInterval - onDismissRequest() - }, - contentPadding = PaddingValues(vertical = 17.dp), - enabled = true, - ) - Spacer(modifier = Modifier.width(7.dp)) - NearSolidTypeButton( - modifier = Modifier.weight(1f), - text = "확인", - onClick = { - onSelectReminderInterval(tempSelected.value) - }, - enabled = true, - contentPadding = PaddingValues(vertical = 17.dp), - ) - } - Spacer(modifier = Modifier.height(24.dp)) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Preview(showBackground = true) -@Composable -fun ReminderIntervalBottomSheetPreview() { - NearTheme { - ReminderIntervalBottomSheet( - sheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded), - ) - } -} From 53cc2218c3be4f5fe4ebee489c80734cf9eba38b Mon Sep 17 00:00:00 2001 From: stopstone Date: Thu, 13 Nov 2025 00:54:39 +0900 Subject: [PATCH 64/95] =?UTF-8?q?fix:=20=EB=8B=A4=EC=9D=8C=20=EC=97=B0?= =?UTF-8?q?=EB=9D=BD=20=EC=A3=BC=EA=B8=B0=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9D=98=20=EA=B3=B5=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CycleSettingBottomSheet` 컴포넌트에서 "다음 연락 주기:" 접두사와 날짜 사이에 공백을 추가하여 가독성을 개선했습니다. --- .../friendcontactcycle/components/CycleSettingBottomSheet.kt | 2 +- .../friendprofileedittor/component/CycleSettingBottomSheet.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt index 9753eafb..99977329 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt @@ -129,7 +129,7 @@ fun CycleSettingBottomSheet( Text( text = - stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + + stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + " " + ( selectedInterval?.let { DateExtension.getNextCycleDate(it) } ?: DateExtension.getNextWeekSameDay() diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt index 26212231..e7f1359d 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt @@ -130,7 +130,7 @@ fun CycleSettingBottomSheet( Text( text = - stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + + stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + " " + ( selectedInterval?.let { DateExtension.getNextCycleDate(it) } ?: DateExtension.getNextWeekSameDay() From 72ac684437d174bce8ae310319f64cfa14f41445 Mon Sep 17 00:00:00 2001 From: stopstone Date: Thu, 13 Nov 2025 00:58:44 +0900 Subject: [PATCH 65/95] =?UTF-8?q?feat:=20`NearBottomSheet`=20=EB=AF=B8?= =?UTF-8?q?=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@Preview` 어노테이션을 사용하여 `NearBottomSheet` 컴포저블의 미리보기를 추가했습니다. --- .../component/bottomsheet/NearBottomSheet.kt | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt index b41ebf42..5480d477 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt @@ -1,14 +1,22 @@ package com.alarmy.near.presentation.ui.component.bottomsheet +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.alarmy.near.presentation.ui.theme.NearTheme @@ -30,11 +38,22 @@ fun NearBottomSheet( onDismissRequest = onDismiss, sheetState = bottomSheetState, dragHandle = { - BottomSheetDefaults.DragHandle( - color = NearTheme.colors.BLACK_1A1A1A.copy(alpha = 0.1f), - width = 36.dp, - height = 5.dp, - ) + Box( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 24.dp), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = + Modifier + .width(36.dp) + .height(5.dp) + .clip(RoundedCornerShape(100.dp)) + .background(NearTheme.colors.BLACK_1A1A1A.copy(alpha = 0.1f)), + ) + } }, modifier = modifier, containerColor = NearTheme.colors.WHITE_FFFFFF, @@ -50,3 +69,16 @@ fun NearBottomSheet( } } } + +@Preview +@Composable +fun NearBottomSheetPreview() { + NearTheme { + NearBottomSheet( + isVisible = true, + onDismiss = { }, + content = {}, + ) + } +} + From 1162e1872a8999c2839db36917db3fd5f13f8b9b Mon Sep 17 00:00:00 2001 From: stopstone Date: Thu, 13 Nov 2025 01:00:37 +0900 Subject: [PATCH 66/95] =?UTF-8?q?chore:=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=BD=94=EB=84=88=20?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NearBottomSheet 핸들러의 모서리 둥글기 값을 100.dp에서 2.5.dp로 수정했습니다. --- .../ui/component/bottomsheet/NearBottomSheet.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt index 5480d477..4c65f54c 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.rememberModalBottomSheetState @@ -50,7 +49,7 @@ fun NearBottomSheet( Modifier .width(36.dp) .height(5.dp) - .clip(RoundedCornerShape(100.dp)) + .clip(RoundedCornerShape(2.5.dp)) .background(NearTheme.colors.BLACK_1A1A1A.copy(alpha = 0.1f)), ) } @@ -76,9 +75,8 @@ fun NearBottomSheetPreview() { NearTheme { NearBottomSheet( isVisible = true, - onDismiss = { }, + onDismiss = { }, content = {}, ) } } - From 4e36099e68f517c54d2316bb322c61f3bcfef0ff Mon Sep 17 00:00:00 2001 From: stopstone Date: Thu, 13 Nov 2025 01:23:41 +0900 Subject: [PATCH 67/95] =?UTF-8?q?refactor:=20=EC=A3=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20BottomSheet=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `friendcontactcycle`과 `friendprofileedittor` 피처에서 각각 사용하던 `CycleSettingBottomSheet`를 `ui/component/bottomsheet` 패키지로 이동하여 공용 컴포넌트로 만들었습니다. - 중복 코드를 제거하고 재사용성을 높였습니다. --- .../components/ContactCycleContent.kt | 1 + .../components/CycleSettingBottomSheet.kt | 209 ------------------ .../FriendProfileEditorScreen.kt | 2 +- .../bottomsheet}/CycleSettingBottomSheet.kt | 16 +- 4 files changed, 6 insertions(+), 222 deletions(-) delete mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt rename Near/app/src/main/java/com/alarmy/near/presentation/{feature/friendprofileedittor/component => ui/component/bottomsheet}/CycleSettingBottomSheet.kt (93%) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactCycleContent.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactCycleContent.kt index 3a4b35b3..0125e1fd 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactCycleContent.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/ContactCycleContent.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp import com.alarmy.near.R import com.alarmy.near.model.ReminderInterval import com.alarmy.near.presentation.feature.friendcontactcycle.model.FriendContactUIModel +import com.alarmy.near.presentation.ui.component.bottomsheet.CycleSettingBottomSheet import com.alarmy.near.presentation.ui.component.checkbox.NearBackgroundCheckbox import com.alarmy.near.presentation.ui.extension.onNoRippleClick import com.alarmy.near.presentation.ui.theme.NearTheme diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt deleted file mode 100644 index 99977329..00000000 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendcontactcycle/components/CycleSettingBottomSheet.kt +++ /dev/null @@ -1,209 +0,0 @@ -package com.alarmy.near.presentation.feature.friendcontactcycle.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.material3.VerticalDivider -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.alarmy.near.R -import com.alarmy.near.model.ReminderInterval -import com.alarmy.near.presentation.ui.component.bottomsheet.NearBottomSheet -import com.alarmy.near.presentation.ui.component.checkbox.NearCheckbox -import com.alarmy.near.presentation.ui.extension.onNoRippleClick -import com.alarmy.near.presentation.ui.theme.NearTheme -import com.alarmy.near.utils.extensions.DateExtension - -@Composable -fun CycleSettingBottomSheet( - isVisible: Boolean, - onDismiss: () -> Unit, - onComplete: (ReminderInterval) -> Unit = {}, - currentSelectedInterval: ReminderInterval? = null, - modifier: Modifier = Modifier, -) { - // 선택된 주기 상태 관리 - var selectedInterval by remember(isVisible) { - mutableStateOf(currentSelectedInterval ?: ReminderInterval.EVERY_WEEK) - } - - NearBottomSheet( - isVisible = isVisible, - onDismiss = onDismiss, - modifier = modifier, - ) { - // 제목 - Text( - text = stringResource(R.string.friend_contact_cycle_cycle_setting_text), - style = NearTheme.typography.B1_16_BOLD, - ) - - Spacer(modifier = Modifier.size(24.dp)) - - // 선택된 주기 미리보기 - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .background(NearTheme.colors.BG02_F4F9FD) - .padding(vertical = 18.dp, horizontal = 20.dp), - ) { - Text( - text = - buildAnnotatedString { - val interval = selectedInterval ?: ReminderInterval.EVERY_WEEK - val cycleText = DateExtension.getCycleText(interval) - val firstSpaceIndex = cycleText.indexOf(' ') - - if (firstSpaceIndex != -1) { - withStyle( - style = - SpanStyle( - color = NearTheme.colors.BLACK_1A1A1A, - fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, - ), - ) { - append(cycleText.substring(0, firstSpaceIndex + 1)) - } - withStyle( - style = - SpanStyle( - color = NearTheme.colors.BLUE01_5AA2E9, - fontWeight = NearTheme.typography.B2_14_BOLD.fontWeight, - ), - ) { - append(cycleText.substring(firstSpaceIndex + 1)) - } - } else { - withStyle( - style = - SpanStyle( - color = NearTheme.colors.BLACK_1A1A1A, - fontWeight = NearTheme.typography.B2_14_MEDIUM.fontWeight, - ), - ) { - append(cycleText) - } - } - }, - style = NearTheme.typography.B2_14_MEDIUM, - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = Modifier.size(20.dp)) - - VerticalDivider( - modifier = - Modifier - .size(width = 1.dp, height = 28.dp), - color = - NearTheme.colors.BLACK_1A1A1A.copy( - alpha = 0.1f, - ), - ) - - Spacer(modifier = Modifier.size(20.dp)) - - Text( - text = - stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + " " + - ( - selectedInterval?.let { DateExtension.getNextCycleDate(it) } - ?: DateExtension.getNextWeekSameDay() - ), - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.GRAY01_888888, - ) - } - } - - Spacer(modifier = Modifier.size(8.dp)) - - // 주기 선택 옵션들 - ReminderInterval.entries.forEach { interval -> - val isSelected = selectedInterval == interval - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier - .fillMaxWidth() - .onNoRippleClick { - if (!isSelected) { - selectedInterval = interval - } - }.padding(vertical = 15.dp), - ) { - Text( - text = stringResource(interval.labelRes), - style = - if (isSelected) { - NearTheme.typography.B1_16_BOLD - } else { - NearTheme.typography.B2_14_MEDIUM - }, - ) - - if (isSelected) { - NearCheckbox( - checked = true, - onCheckedChange = { checked -> - // 체크박스 클릭 시 해제되지 않도록 수정 - // 체크된 상태를 유지 - }, - ) - } - } - } - Spacer(modifier = Modifier.size(24.dp)) - - // 버튼들 - ContactCycleButtons( - onLeftButtonClick = onDismiss, - onRightButtonClick = { - selectedInterval?.let { interval -> - onComplete(interval) - onDismiss() - } - }, - leftButtonText = stringResource(R.string.friend_contact_cycle_cancel_button), - rightButtonText = stringResource(R.string.friend_contact_cycle_complete_button), - ) - Spacer(modifier = Modifier.size(24.dp)) - } -} - -@Preview -@Composable -fun CycleSettingBottomSheetPreview() { - NearTheme { - CycleSettingBottomSheet( - isVisible = true, - onDismiss = {}, - ) - } -} diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt index af922137..aab62b35 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt @@ -44,8 +44,8 @@ import com.alarmy.near.model.DayOfWeek import com.alarmy.near.model.Friend import com.alarmy.near.model.Relation import com.alarmy.near.model.ReminderInterval -import com.alarmy.near.presentation.feature.friendcontactcycle.components.CycleSettingBottomSheet import com.alarmy.near.presentation.feature.friendprofileedittor.component.NearDatePicker +import com.alarmy.near.presentation.ui.component.bottomsheet.CycleSettingBottomSheet import com.alarmy.near.presentation.feature.friendprofileedittor.dialog.EditorExitDialog import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.FriendProfileEditorUIEvent import com.alarmy.near.presentation.feature.friendprofileedittor.uistate.FriendProfileEditorUIState diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/CycleSettingBottomSheet.kt similarity index 93% rename from Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt rename to Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/CycleSettingBottomSheet.kt index e7f1359d..394b7cb9 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/component/CycleSettingBottomSheet.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/CycleSettingBottomSheet.kt @@ -1,4 +1,4 @@ -package com.alarmy.near.presentation.feature.friendprofileedittor.component +package com.alarmy.near.presentation.ui.component.bottomsheet import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -27,7 +27,6 @@ import androidx.compose.ui.unit.dp import com.alarmy.near.R import com.alarmy.near.model.ReminderInterval import com.alarmy.near.presentation.feature.friendcontactcycle.components.ContactCycleButtons -import com.alarmy.near.presentation.ui.component.bottomsheet.NearBottomSheet import com.alarmy.near.presentation.ui.component.checkbox.NearCheckbox import com.alarmy.near.presentation.ui.extension.onNoRippleClick import com.alarmy.near.presentation.ui.theme.NearTheme @@ -41,7 +40,6 @@ fun CycleSettingBottomSheet( currentSelectedInterval: ReminderInterval? = null, modifier: Modifier = Modifier, ) { - // 선택된 주기 상태 관리 var selectedInterval by remember(isVisible) { mutableStateOf(currentSelectedInterval ?: ReminderInterval.EVERY_WEEK) } @@ -51,7 +49,6 @@ fun CycleSettingBottomSheet( onDismiss = onDismiss, modifier = modifier, ) { - // 제목 Text( text = stringResource(R.string.friend_contact_cycle_cycle_setting_text), style = NearTheme.typography.B1_16_BOLD, @@ -59,7 +56,6 @@ fun CycleSettingBottomSheet( Spacer(modifier = Modifier.size(24.dp)) - // 선택된 주기 미리보기 Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -130,7 +126,7 @@ fun CycleSettingBottomSheet( Text( text = - stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + " " + + stringResource(R.string.friend_contact_cycle_next_cycle_prefix) + ( selectedInterval?.let { DateExtension.getNextCycleDate(it) } ?: DateExtension.getNextWeekSameDay() @@ -143,7 +139,6 @@ fun CycleSettingBottomSheet( Spacer(modifier = Modifier.size(8.dp)) - // 주기 선택 옵션들 ReminderInterval.entries.forEach { interval -> val isSelected = selectedInterval == interval @@ -172,17 +167,13 @@ fun CycleSettingBottomSheet( if (isSelected) { NearCheckbox( checked = true, - onCheckedChange = { checked -> - // 체크박스 클릭 시 해제되지 않도록 수정 - // 체크된 상태를 유지 - }, + onCheckedChange = {}, ) } } } Spacer(modifier = Modifier.size(24.dp)) - // 버튼들 ContactCycleButtons( onLeftButtonClick = onDismiss, onRightButtonClick = { @@ -208,3 +199,4 @@ fun CycleSettingBottomSheetPreview() { ) } } + From add9b61e302164548bb4abbbed7eca8f8c9046b9 Mon Sep 17 00:00:00 2001 From: stopstone Date: Thu, 13 Nov 2025 01:24:48 +0900 Subject: [PATCH 68/95] =?UTF-8?q?refactor:=20NearBottomSheet=EC=9D=98=20co?= =?UTF-8?q?ntent=20=ED=83=80=EC=9E=85=EC=9D=84=20ColumnScope=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/component/bottomsheet/NearBottomSheet.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt index 4c65f54c..191162cf 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/bottomsheet/NearBottomSheet.kt @@ -3,6 +3,7 @@ package com.alarmy.near.presentation.ui.component.bottomsheet import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -25,7 +26,7 @@ fun NearBottomSheet( isVisible: Boolean, onDismiss: () -> Unit, modifier: Modifier = Modifier, - content: @Composable () -> Unit, + content: @Composable ColumnScope.() -> Unit, ) { if (isVisible) { val bottomSheetState = From b83ad29f5235bfa7a006cf3a3cd2bc80a1deacd8 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Mon, 17 Nov 2025 23:26:58 +0900 Subject: [PATCH 69/95] =?UTF-8?q?feat:=20monthlyFriend=ED=82=A4=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/alarmy/near/presentation/feature/home/HomeScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 43e06e07..2e8f9544 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -215,9 +215,9 @@ internal fun HomeScreen( ) { items( count = monthlyFriends.size, - key = { - "monthly_friend_${monthlyFriends[it].friendId}" - }, +// key = { +// "monthly_friend_${monthlyFriends[it].friendId}" +// }, ) { val monthlyContact = monthlyFriends[it] val now = LocalDate.now() From ad3cfe9253e5db43d8d3ec83c80d947aa6dbeb5b Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Mon, 17 Nov 2025 23:34:27 +0900 Subject: [PATCH 70/95] =?UTF-8?q?fix:=20=EC=B1=99=EA=B9=80=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=EC=9D=B4=20=EC=A0=81=EC=9A=A9=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alarmy/near/data/mapper/FriendMapper.kt | 2 +- .../friendprofile/FriendProfileViewModel.kt | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt index b11dc98f..04f91912 100644 --- a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt +++ b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt @@ -28,8 +28,8 @@ fun FriendEntity.toModel(): Friend = anniversaryList = anniversaryList.map { it.toModel() }, memo = memo, phone = phone, - isContactToday = lastContactAt?.isToday() ?: false, lastContactAt = lastContactAt, + isContactToday = lastContactAt?.isToday() ?: false, lastContactFormat = lastContactAt?.contactFormat(), ) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt index 4a766269..90926c87 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.Locale @@ -118,6 +117,10 @@ class FriendProfileViewModel friend = it.friend.copy( lastContactAt = getTodayDashFormat(), + lastContactFormat = it.friend.lastContactAt?.contactFormat(), + isContactToday = + it.friend.lastContactAt?.isToday() + ?: false, ), ) } @@ -146,4 +149,19 @@ class FriendProfileViewModel _friendFlow.update { (it as FriendState.Success).copy(friend = friend) } } } + + fun String.contactFormat(): String { + val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val outputFormatter = DateTimeFormatter.ofPattern("M월 d일") + + val date = LocalDate.parse(this, inputFormatter) + return date.format(outputFormatter) + } + + private fun String.isToday(): Boolean { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.KOREA) + val targetDate = LocalDate.parse(this, formatter) + val today = LocalDate.now() + return targetDate == today } +} From 1f4474b144668d7ddcd9c9a1e5d2282d0329163e Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Mon, 17 Nov 2025 23:57:03 +0900 Subject: [PATCH 71/95] =?UTF-8?q?fix:=20=EB=A6=AC=ED=94=8C=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friendprofile/FriendProfileScreen.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt index 16d2a3a3..9ca3720d 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt @@ -21,8 +21,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider @@ -52,7 +50,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.times import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -344,7 +341,11 @@ fun FriendProfileScreen( Modifier .width(85.dp) .height(50.dp), - selected = currentTabPosition.intValue == 0, + selectedContentColor = + NearTheme.colors.GRAY01_888888.copy( + alpha = 0.3f, + ), + selected = currentTabPosition.intValue == 0, onClick = { currentTabPosition.intValue = 0 }, ) { Text( @@ -374,6 +375,9 @@ fun FriendProfileScreen( .height(50.dp), selected = currentTabPosition.intValue == 1, onClick = { currentTabPosition.intValue = 1 }, + selectedContentColor = NearTheme.colors.GRAY01_888888.copy( + alpha = 0.3f + ), ) { Text( text = stringResource(R.string.friend_profile_tab_text_record), @@ -424,8 +428,8 @@ fun FriendProfileScreen( brush = Brush.linearGradient( colors = listOf(Color(0x00FFFFFF), Color(0xFFFFFFFF)), // 파랑 → 밝은 하늘색 - start = Offset(0f, 0f), // 위쪽 시작 - end = Offset(0f, Float.POSITIVE_INFINITY) + start = Offset(0f, 0f), // 위쪽 시작 + end = Offset(0f, Float.POSITIVE_INFINITY), ), ), ) { @@ -614,7 +618,8 @@ private fun RecordItem( contentDescription = null, ) Text( - stringResource(R.string.friend_profile_info_contact_record_text, index), + text = stringResource(R.string.friend_profile_info_contact_record_text, index), + textAlign = TextAlign.Center, style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.BLUE01_5AA2E9, ) From 8040d8f0e430ae8a29e3eb3708bab07238919691 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 6 Nov 2025 01:39:05 +0900 Subject: [PATCH 72/95] =?UTF-8?q?feat:=20shimmer=20modifier=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/extension/NearShimmer.kt | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt new file mode 100644 index 00000000..e418a6ed --- /dev/null +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt @@ -0,0 +1,101 @@ +package com.alarmy.near.presentation.ui.extension + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +internal enum class ShimmerType( + val colors: List, +) { + PRIMARY( + listOf( + Color(0xff5997E5), + Color(0xffA9D8FF), + ), + ), + WHITE( + listOf( + Color(0xffFFFFFF), + Color(0xffEBEBEB), + ), + ), +} + +@Composable +fun NearConditionalShimmer( + enabled: Boolean, + modifier: Modifier = Modifier, + shimmerColors: List = ShimmerType.PRIMARY.colors, + cornerRadius: Dp = 12.dp, + content: @Composable () -> Unit, +) { + Box(modifier = modifier) { + // 항상 content를 렌더링해서 크기 확보 (투명하게) + Box( + modifier = Modifier.alpha(if (enabled) 0f else 1f), + ) { + content() + } + + // shimmer가 enabled일 때만 효과 표시 + if (enabled) { + NearShimmerEffect( + modifier = Modifier.matchParentSize(), + shimmerColors = shimmerColors, + cornerRadius = cornerRadius, + ) + } + } +} + +@Composable +private fun NearShimmerEffect( + modifier: Modifier = Modifier, + shimmerColors: List = ShimmerType.PRIMARY.colors, + cornerRadius: Dp = 12.dp, +) { + val transition = rememberInfiniteTransition(label = "shimmer") + val translateAnim = + transition.animateFloat( + initialValue = 0f, + targetValue = 1000f, + animationSpec = + infiniteRepeatable( + animation = tween(durationMillis = 1000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "shimmer_translate", + ) + + val brush = + remember(translateAnim.value) { + Brush.linearGradient( + colors = shimmerColors, + start = Offset.Zero, + end = Offset(translateAnim.value, translateAnim.value), + ) + } + + Box( + modifier = + modifier + .clip(RoundedCornerShape(cornerRadius)) + .background(brush), + ) +} From 576bc85506a14ad4593e9f8725ab42f9df876622 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 6 Nov 2025 01:41:46 +0900 Subject: [PATCH 73/95] =?UTF-8?q?fix:=20=EA=B0=80=EB=A1=9C=20shimmer?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/extension/NearShimmer.kt | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt index e418a6ed..0c2711fb 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/extension/NearShimmer.kt @@ -1,6 +1,7 @@ package com.alarmy.near.presentation.ui.extension import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable @@ -10,13 +11,18 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -70,32 +76,41 @@ private fun NearShimmerEffect( shimmerColors: List = ShimmerType.PRIMARY.colors, cornerRadius: Dp = 12.dp, ) { + var componentWidth by remember { mutableFloatStateOf(0f) } + val transition = rememberInfiniteTransition(label = "shimmer") val translateAnim = transition.animateFloat( initialValue = 0f, - targetValue = 1000f, + targetValue = 1f, animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1000, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse, + animation = tween(durationMillis = 1000, easing = LinearEasing), + repeatMode = RepeatMode.Restart, ), label = "shimmer_translate", ) val brush = - remember(translateAnim.value) { - Brush.linearGradient( - colors = shimmerColors, - start = Offset.Zero, - end = Offset(translateAnim.value, translateAnim.value), - ) + remember(translateAnim.value, componentWidth, shimmerColors) { + if (componentWidth > 0f) { + val offset = translateAnim.value * componentWidth * 2 - componentWidth + Brush.linearGradient( + colors = shimmerColors, + start = Offset(x = offset, y = 0f), + end = Offset(x = offset + componentWidth, y = 0f), + ) + } else { + Brush.linearGradient(colors = shimmerColors) + } } Box( modifier = modifier - .clip(RoundedCornerShape(cornerRadius)) + .onGloballyPositioned { coordinates -> + componentWidth = coordinates.size.width.toFloat() + }.clip(RoundedCornerShape(cornerRadius)) .background(brush), ) } From 01267f0513bb55f9aa3aed902dad7b32f3cf17ac Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 6 Nov 2025 01:42:09 +0900 Subject: [PATCH 74/95] =?UTF-8?q?refactor:=20shimmer=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EB=B0=8F=20home=20uiState=EB=A1=9C=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/feature/home/HomeScreen.kt | 438 ++++++++++-------- .../feature/home/HomeViewModel.kt | 99 ++-- .../feature/home/model/HomeUiState.kt | 26 +- 3 files changed, 310 insertions(+), 253 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 65d1cc24..02d79d50 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -54,16 +54,19 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.alarmy.near.R import com.alarmy.near.model.friendsummary.ContactFrequencyLevel import com.alarmy.near.model.friendsummary.FriendSummary -import com.alarmy.near.model.member.MemberInfo import com.alarmy.near.model.monthly.MonthlyFriend import com.alarmy.near.model.monthly.MonthlyFriendType import com.alarmy.near.presentation.feature.home.component.MyContacts +import com.alarmy.near.presentation.feature.home.model.HomeUiState +import com.alarmy.near.presentation.feature.home.model.MonthlyFriendUIState +import com.alarmy.near.presentation.feature.home.model.MyFriendUIState import com.alarmy.near.presentation.ui.component.dropdown.NearDropdownMenu import com.alarmy.near.presentation.ui.component.dropdown.NearDropdownMenuItem +import com.alarmy.near.presentation.ui.extension.NearConditionalShimmer +import com.alarmy.near.presentation.ui.extension.ShimmerType import com.alarmy.near.presentation.ui.extension.dropShadow import com.alarmy.near.presentation.ui.extension.onNoRippleClick import com.alarmy.near.presentation.ui.theme.NearTheme -import kotlinx.coroutines.launch import java.time.LocalDate private const val MINIMUM_PAGE_COUNT_TO_SHOW_UI = 2 @@ -79,24 +82,18 @@ internal fun HomeRoute( onMonthlyReminderAllClick: () -> Unit = {}, ) { LaunchedEffect(Unit) { - launch { - viewModel.errorEvent.collect { - onShowErrorSnackBar(IllegalStateException("네트워크 에러가 발생했습니다.")) - } + viewModel.errorEvent.collect { + onShowErrorSnackBar(IllegalStateException("네트워크 에러가 발생했습니다.")) } } - val memberInfo = viewModel.memberInfoFlow.collectAsStateWithLifecycle() - val friends = viewModel.friendsFlow.collectAsStateWithLifecycle() - val monthlyFriends = viewModel.monthlyFriendFlow.collectAsStateWithLifecycle() + val uiState = viewModel.uiState.collectAsStateWithLifecycle() HomeScreen( + uiState = uiState.value, onContactClick = onContactClick, onAlarmClick = onAlarmClick, onMyPageClick = onMyPageClick, onAddContactClick = onAddContactClick, onMonthlyReminderAllClick = onMonthlyReminderAllClick, - contacts = friends.value, - monthlyFriends = monthlyFriends.value, - memberInfo = memberInfo.value, ) } @@ -109,21 +106,10 @@ internal fun HomeScreen( onAlarmClick: () -> Unit = {}, onAddContactClick: () -> Unit = {}, onMonthlyReminderAllClick: () -> Unit = {}, - memberInfo: MemberInfo?, - contacts: List, - monthlyFriends: List, + uiState: HomeUiState, ) { val density = LocalDensity.current val statusBarHeightDp = with(density) { WindowInsets.statusBars.getTop(density).toDp() } - val contactsWithPage = contacts.chunked(5) - val pagerState: PagerState = - rememberPagerState( - initialPage = 0, - pageCount = { - contactsWithPage.count() + if (contactsWithPage.lastOrNull()?.count() == 5) 1 else 0 - }, - ) - val dropdownState = remember { mutableStateOf(false) } Surface(modifier = modifier) { Column( @@ -162,25 +148,25 @@ internal fun HomeScreen( ) } Spacer(modifier = Modifier.height(16.dp)) - Text( - text = - buildAnnotatedString { - append("${memberInfo?.nickname}님,\n") - withStyle( - SpanStyle( - fontWeight = FontWeight.Bold, - ), - ) { - append("누구를 챙길지") - } - append(" 정해볼까요?") - }, - modifier = Modifier.padding(horizontal = 24.dp), - style = NearTheme.typography.H1_24_REGULAR, - color = NearTheme.colors.WHITE_FFFFFF, - ) - Spacer(modifier = Modifier.height(32.dp)) + Box(modifier = Modifier.padding(horizontal = 24.dp)) { + NearConditionalShimmer(enabled = uiState.memberInfo == null) { + Text( + text = + buildAnnotatedString { + append("${uiState.memberInfo?.nickname}님,\n") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append("누구를 챙길지") + } + append(" 정해볼까요?") + }, + style = NearTheme.typography.H1_24_REGULAR, + color = NearTheme.colors.WHITE_FFFFFF, + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -198,90 +184,103 @@ internal fun HomeScreen( ) } Spacer(modifier = Modifier.height(16.dp)) - if (monthlyFriends.isEmpty()) { - Surface( + + if (uiState.monthlyFriendUIState is MonthlyFriendUIState.Loading) { + NearConditionalShimmer( + enabled = true, modifier = Modifier - .fillMaxWidth() .padding(horizontal = 20.dp) - .clip(RoundedCornerShape(12.dp)), - color = NearTheme.colors.WHITE_FFFFFF.copy(alpha = 0.2f), - ) { - Text( - text = stringResource(R.string.home_no_people_this_month), + .fillMaxWidth() + .height(48.dp), + ) {} + } else if (uiState.monthlyFriendUIState is MonthlyFriendUIState.Success) { + if (uiState.monthlyFriendUIState.monthlyFriends.isEmpty()) { + Surface( modifier = Modifier .fillMaxWidth() - .padding(vertical = 14.dp), - textAlign = TextAlign.Center, - style = - NearTheme.typography.B2_14_MEDIUM.copy( - fontWeight = FontWeight.Normal, - ), - color = NearTheme.colors.WHITE_FFFFFF, - ) - } - } else { - LazyRow( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - items( - count = monthlyFriends.size, - key = { - monthlyFriends[it].friendId - }, + .padding(horizontal = 20.dp) + .clip(RoundedCornerShape(12.dp)), + color = NearTheme.colors.WHITE_FFFFFF.copy(alpha = 0.2f), ) { - val monthlyContact = monthlyFriends[it] - val now = LocalDate.now() - Surface( - modifier.dropShadow( - shape = RoundedCornerShape(12.dp), - color = Color.Black.copy(alpha = 0.07f), - blur = 4.dp, - offsetY = 4.dp, - ), + Text( + text = stringResource(R.string.home_no_people_this_month), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 14.dp), + textAlign = TextAlign.Center, + style = + NearTheme.typography.B2_14_MEDIUM.copy( + fontWeight = FontWeight.Normal, + ), color = NearTheme.colors.WHITE_FFFFFF, - shape = RoundedCornerShape(12.dp), + ) + } + } else { + val monthlyFriends = uiState.monthlyFriendUIState.monthlyFriends + LazyRow( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + count = monthlyFriends.size, + key = { + monthlyFriends[it].friendId + }, ) { - Row( - modifier = - Modifier - .padding(start = 12.dp, end = 16.dp) - .padding(vertical = 12.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + val monthlyContact = monthlyFriends[it] + val now = LocalDate.now() + Surface( + modifier.dropShadow( + shape = RoundedCornerShape(12.dp), + color = Color.Black.copy(alpha = 0.07f), + blur = 4.dp, + offsetY = 4.dp, + ), + color = NearTheme.colors.WHITE_FFFFFF, + shape = RoundedCornerShape(12.dp), ) { - Image( - painterResource(monthlyContact.type.imageSrc), - contentDescription = "", - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - modifier = Modifier.widthIn(max = 97.dp), - text = monthlyContact.name, - style = NearTheme.typography.B2_14_BOLD, - textAlign = TextAlign.Center, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - color = NearTheme.colors.BLACK_1A1A1A, - ) - Spacer(modifier = Modifier.width(12.dp)) - if (monthlyContact.isNextContactDay(now)) { - Text( - text = monthlyContact.daysUntilNextContact(LocalDate.now()), - style = NearTheme.typography.B2_14_BOLD, - color = NearTheme.colors.BLUE01_5AA2E9, + Row( + modifier = + Modifier + .padding(start = 12.dp, end = 16.dp) + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painterResource(monthlyContact.type.imageSrc), + contentDescription = "", ) - } else { + Spacer(modifier = Modifier.width(8.dp)) Text( - text = monthlyContact.daysUntilNextContact(now), - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.BLACK_1A1A1A.copy(alpha = 0.5f), + modifier = Modifier.widthIn(max = 97.dp), + text = monthlyContact.name, + style = NearTheme.typography.B2_14_BOLD, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + color = NearTheme.colors.BLACK_1A1A1A, ) + Spacer(modifier = Modifier.width(12.dp)) + if (monthlyContact.isNextContactDay(now)) { + Text( + text = monthlyContact.daysUntilNextContact(LocalDate.now()), + style = NearTheme.typography.B2_14_BOLD, + color = NearTheme.colors.BLUE01_5AA2E9, + ) + } else { + Text( + text = monthlyContact.daysUntilNextContact(now), + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.BLACK_1A1A1A.copy(alpha = 0.5f), + ) + } } } } @@ -291,68 +290,116 @@ internal fun HomeScreen( Spacer(modifier = Modifier.height(24.dp)) - Box( - modifier = - Modifier - .fillMaxSize() - .background( - color = NearTheme.colors.WHITE_FFFFFF, - shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), - ), - ) { - MyContacts( - modifier = Modifier.align(Alignment.TopCenter), - contactsWithPage = contactsWithPage, - pagerState = pagerState, - onContactClick = onContactClick, - onAddContactClick = { - onAddContactClick() - }, - ) - Row( + MyFriends( + onContactClick = onContactClick, + onAddContactClick = onAddContactClick, + myFriendUIState = uiState.myFriendUIState, + ) + } + } +} + +@Composable +private fun MyFriends( + myFriendUIState: MyFriendUIState, + onContactClick: (String) -> Unit, + onAddContactClick: () -> Unit, +) { + val contactsWithPage = + if (myFriendUIState is MyFriendUIState.Success) myFriendUIState.myFriends.chunked(5) else listOf() + val pagerState: PagerState = + rememberPagerState( + initialPage = 0, + pageCount = { + contactsWithPage.count() + + if (contactsWithPage + .lastOrNull() + ?.count() == 5 + ) { + 1 + } else { + 0 + } + }, + ) + val dropdownState = remember { mutableStateOf(false) } + + Box( + modifier = + Modifier + .fillMaxSize() + .background( + color = NearTheme.colors.WHITE_FFFFFF, + shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), + ), + ) { + if (myFriendUIState is MyFriendUIState.Loading) { + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(166.dp)) + NearConditionalShimmer( + shimmerColors = ShimmerType.WHITE.colors, + modifier = Modifier.size(width = 151.dp, height = 125.dp), + enabled = true, + ) { + } + } + } else { + MyContacts( + modifier = Modifier.align(Alignment.TopCenter), + contactsWithPage = contactsWithPage, + pagerState = pagerState, + onContactClick = onContactClick, + onAddContactClick = { + onAddContactClick() + }, + ) + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.TopStart) + .padding(top = 20.dp, start = 24.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + stringResource(R.string.home_my_people), + style = NearTheme.typography.H2_18_BOLD, + color = NearTheme.colors.BLACK_1A1A1A, + ) + Column { + Image( modifier = - Modifier - .fillMaxWidth() - .align(Alignment.TopStart) - .padding(top = 20.dp, start = 24.dp, end = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, + Modifier.onNoRippleClick(onClick = { + dropdownState.value = true + }), + painter = painterResource(R.drawable.ic_32_menu), + contentDescription = stringResource(R.string.home_my_people_setting), + ) + NearDropdownMenu( + expanded = dropdownState.value, + onDismissRequest = { dropdownState.value = false }, ) { - Text( - stringResource(R.string.home_my_people), - style = NearTheme.typography.H2_18_BOLD, - color = NearTheme.colors.BLACK_1A1A1A, + NearDropdownMenuItem( + onClick = { + onAddContactClick() + dropdownState.value = false + }, + text = stringResource(R.string.home_menu_text_add_friend), ) - Column { - Image( - modifier = - Modifier.onNoRippleClick(onClick = { - dropdownState.value = true - }), - painter = painterResource(R.drawable.ic_32_menu), - contentDescription = stringResource(R.string.home_my_people_setting), - ) - NearDropdownMenu( - expanded = dropdownState.value, - onDismissRequest = { dropdownState.value = false }, - ) { - NearDropdownMenuItem( - onClick = { - onAddContactClick() - dropdownState.value = false - }, - text = stringResource(R.string.home_menu_text_add_friend), - ) - } - } } + } + } - if (contactsWithPage.size >= MINIMUM_PAGE_COUNT_TO_SHOW_UI) { - Column(modifier = Modifier.align(Alignment.BottomCenter)) { - PagerIndicator(pagerState) - Spacer(modifier = Modifier.height(104.dp)) - } - } + if (contactsWithPage.size >= MINIMUM_PAGE_COUNT_TO_SHOW_UI) { + Column(modifier = Modifier.align(Alignment.BottomCenter)) { + PagerIndicator(pagerState) + Spacer(modifier = Modifier.height(104.dp)) } } } @@ -423,43 +470,22 @@ internal fun HomeScreenPreview() { NearTheme { HomeScreen( onContactClick = {}, - contacts = - List(6) { - FriendSummary( - id = "2003", - name = "일이삼사오육칠팔구", - profileImageUrl = "https://search.yahoo.com/search?p=partiendo", - lastContactedAt = "2025-07-16", - isContacted = false, - contactFrequencyLevel = ContactFrequencyLevel.HIGH, - ) - }, - monthlyFriends = - List(4) { - MonthlyFriend( - friendId = "intellegat$it", - name = "Stacey Stewart", - type = MonthlyFriendType.ANNIVERSARY, - nextContactAt = "2025-09-30", - ) - }, - memberInfo = - MemberInfo( - memberId = "posidonium", - username = "주지스님", - nickname = "Audra Day", - imageUrl = "https://search.yahoo.com/search?p=class", - notificationAgreedAt = "comprehensam", - providerType = "sumo", + uiState = + HomeUiState( + myFriendUIState = + MyFriendUIState.Loading, + monthlyFriendUIState = + MonthlyFriendUIState.Success(List(4) { + MonthlyFriend( + friendId = "intellegat$it", + name = "Stacey Stewart", + type = MonthlyFriendType.ANNIVERSARY, + nextContactAt = "2025-09-30", + ) + },), + memberInfo = + null, ), ) } } - -@Preview -@Composable -fun MonthlyReminderFriendsViewAllPreview() { - NearTheme { - MonthlyReminderFriendsViewAll() - } -} diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt index d8681d00..c52e39c8 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt @@ -1,22 +1,21 @@ package com.alarmy.near.presentation.feature.home -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.alarmy.near.data.repository.FriendRepository import com.alarmy.near.data.repository.MemberRepository -import com.alarmy.near.model.friendsummary.FriendSummary -import com.alarmy.near.model.member.MemberInfo -import com.alarmy.near.model.monthly.MonthlyFriend +import com.alarmy.near.presentation.feature.home.model.HomeUiState +import com.alarmy.near.presentation.feature.home.model.MonthlyFriendUIState +import com.alarmy.near.presentation.feature.home.model.MyFriendUIState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -27,48 +26,62 @@ class HomeViewModel friendRepository: FriendRepository, memberRepository: MemberRepository, ) : ViewModel() { + private val _uiState = MutableStateFlow(HomeUiState()) + val uiState = _uiState.asStateFlow() + private val _errorEvent = Channel() private val deletedFriendIdsFlow = MutableStateFlow>(setOf()) val errorEvent = _errorEvent.receiveAsFlow() - val memberInfoFlow: StateFlow = - memberRepository - .getMyInfo() - .catch { - _errorEvent.send(it) - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5_000), - null, - ) - - val friendsFlow: StateFlow> = - combine( - friendRepository - .fetchFriends(), - deletedFriendIdsFlow, - ) { friends, deletedIds -> - friends.filter { it.id !in deletedIds } - }.catch { - _errorEvent.send(it) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) - val monthlyFriendFlow: - StateFlow> = - friendRepository - .fetchMonthlyFriends() - .catch { - _errorEvent.send(it) - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = emptyList(), - ) + init { + viewModelScope.launch { + launch { + memberRepository + .getMyInfo() + .catch { + _errorEvent.send(it) + }.collect { + _uiState.update { state -> + state.copy( + memberInfo = it, + ) + } + } + } + launch { + combine( + friendRepository + .fetchFriends(), + deletedFriendIdsFlow, + ) { friends, deletedIds -> + friends.filter { it.id !in deletedIds } + }.catch { + _errorEvent.send(it) + }.collect { + _uiState.update { state -> + state.copy( + myFriendUIState = MyFriendUIState.Success(it), + ) + } + } + } + launch { + friendRepository + .fetchMonthlyFriends() + .catch { + _errorEvent.send(it) + }.collect { + _uiState.update { state -> + state.copy( + monthlyFriendUIState = MonthlyFriendUIState.Success(it), + ) + } + } + } + } + } fun deleteFriend(friendId: String) { viewModelScope.launch { diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/model/HomeUiState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/model/HomeUiState.kt index c4510aef..5396dd0c 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/model/HomeUiState.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/model/HomeUiState.kt @@ -1,9 +1,27 @@ package com.alarmy.near.presentation.feature.home.model -sealed interface HomeUiState { - data object Loading : HomeUiState +import com.alarmy.near.model.friendsummary.FriendSummary +import com.alarmy.near.model.member.MemberInfo +import com.alarmy.near.model.monthly.MonthlyFriend +data class HomeUiState( + val memberInfo: MemberInfo? = null, + val monthlyFriendUIState: MonthlyFriendUIState = MonthlyFriendUIState.Loading, + val myFriendUIState: MyFriendUIState = MyFriendUIState.Loading, +) + +sealed interface MonthlyFriendUIState { + data class Success( + val monthlyFriends: List, + ) : MonthlyFriendUIState + + data object Loading : MonthlyFriendUIState +} + +sealed interface MyFriendUIState { data class Success( - val data: Any, - ) : HomeUiState + val myFriends: List, + ) : MyFriendUIState + + data object Loading : MyFriendUIState } From 0044d8712b6210c173523fbe8da7709ff8ee1dcf Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 6 Nov 2025 01:45:14 +0900 Subject: [PATCH 75/95] =?UTF-8?q?fix:=20=EC=9D=B4=EB=B2=88=20=EB=8B=AC=20?= =?UTF-8?q?=EC=B1=99=EA=B8=B8=20=EC=82=AC=EB=9E=8C=20=EC=9A=B0=EC=B8=A1=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/alarmy/near/presentation/feature/home/HomeScreen.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 02d79d50..566e4d07 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -5,6 +5,7 @@ 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.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -221,10 +222,10 @@ internal fun HomeScreen( } else { val monthlyFriends = uiState.monthlyFriendUIState.monthlyFriends LazyRow( + contentPadding = PaddingValues(horizontal = 20.dp), modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), + .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { items( From 3a7bed5a83cdbf3e41041cd285b56563481d509f Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 6 Nov 2025 01:48:09 +0900 Subject: [PATCH 76/95] =?UTF-8?q?chore:=20home=20preview=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/feature/home/HomeScreen.kt | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 566e4d07..129204f9 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -55,6 +55,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.alarmy.near.R import com.alarmy.near.model.friendsummary.ContactFrequencyLevel import com.alarmy.near.model.friendsummary.FriendSummary +import com.alarmy.near.model.member.MemberInfo import com.alarmy.near.model.monthly.MonthlyFriend import com.alarmy.near.model.monthly.MonthlyFriendType import com.alarmy.near.presentation.feature.home.component.MyContacts @@ -474,7 +475,18 @@ internal fun HomeScreenPreview() { uiState = HomeUiState( myFriendUIState = - MyFriendUIState.Loading, + MyFriendUIState.Success( + List(5) { + FriendSummary( + id = "2003", + name = "일이삼사오육칠팔구", + profileImageUrl = "https://search.yahoo.com/search?p=partiendo", + lastContactedAt = "2025-07-16", + isContacted = false, + contactFrequencyLevel = ContactFrequencyLevel.HIGH, + ) + }, + ), monthlyFriendUIState = MonthlyFriendUIState.Success(List(4) { MonthlyFriend( @@ -485,7 +497,14 @@ internal fun HomeScreenPreview() { ) },), memberInfo = - null, + MemberInfo( + memberId = "posidonium", + username = "주지스님", + nickname = "Audra Day", + imageUrl = "https://search.yahoo.com/search?p=class", + notificationAgreedAt = "comprehensam", + providerType = "sumo", + ), ), ) } From ca872a85657dd99e3a9e674d7d3289141622549e Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 01:31:10 +0900 Subject: [PATCH 77/95] =?UTF-8?q?fix:=20inset=20=ED=8C=A8=EB=94=A9=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EC=9E=91=EC=9D=80=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=ED=99=94=EB=A9=B4=20=EB=8C=80=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/onboarding/OnboardingScreen.kt | 158 ++++++++++-------- 1 file changed, 84 insertions(+), 74 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt index 48928afc..e4f85707 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/onboarding/OnboardingScreen.kt @@ -6,12 +6,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -21,8 +23,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -40,6 +42,7 @@ import com.alarmy.near.presentation.feature.onboarding.components.BackgroundArea import com.alarmy.near.presentation.feature.onboarding.components.OnboardingButton import com.alarmy.near.presentation.feature.onboarding.components.PageIndicator import com.alarmy.near.presentation.feature.onboarding.model.OnboardingPage +import com.alarmy.near.presentation.ui.component.NearFrame import com.alarmy.near.presentation.ui.theme.NearTheme import kotlinx.coroutines.launch @@ -67,30 +70,31 @@ fun OnboardingScreen( } // 온보딩 페이지 데이터 - remember로 성능 최적화 - val pages = remember { - listOf( - OnboardingPage( - titleResId = R.string.first_onboarding_title, - image = R.drawable.img_onboarding_page_first, - ), - OnboardingPage( - titleResId = R.string.second_onboarding_title, - image = R.drawable.img_onboarding_page_second, - ), - OnboardingPage( - titleResId = R.string.third_onboarding_title, - image = R.drawable.img_onboarding_page_third, - ), - OnboardingPage( - titleResId = R.string.fourth_onboarding_title, - image = R.drawable.img_onboarding_page_forth, - ), - OnboardingPage( - titleResId = R.string.fifth_onboarding_title, - image = R.drawable.img_onboarding_page_fifth, - ), - ) - } + val pages = + remember { + listOf( + OnboardingPage( + titleResId = R.string.first_onboarding_title, + image = R.drawable.img_onboarding_page_first, + ), + OnboardingPage( + titleResId = R.string.second_onboarding_title, + image = R.drawable.img_onboarding_page_second, + ), + OnboardingPage( + titleResId = R.string.third_onboarding_title, + image = R.drawable.img_onboarding_page_third, + ), + OnboardingPage( + titleResId = R.string.fourth_onboarding_title, + image = R.drawable.img_onboarding_page_forth, + ), + OnboardingPage( + titleResId = R.string.fifth_onboarding_title, + image = R.drawable.img_onboarding_page_fifth, + ), + ) + } // 상태바와 네비게이션 바 높이 계산 val density = LocalDensity.current @@ -101,53 +105,56 @@ fun OnboardingScreen( val pagerState = rememberPagerState(pageCount = { pages.size }) val scope = rememberCoroutineScope() - Box( - modifier = Modifier.fillMaxSize(), - ) { - BackgroundArea() - Column( - modifier = Modifier - .padding(top = statusBarHeightDp, bottom = navigationBarHeightDp), - horizontalAlignment = Alignment.CenterHorizontally, + NearFrame { + Box( + modifier = Modifier.fillMaxSize(), ) { - // 뷰페이저 - HorizontalPager( - state = pagerState, - modifier = Modifier.weight(1f), - ) { page -> - OnboardingPageContent( - page = pages[page], - modifier = Modifier.fillMaxSize(), - ) - } + BackgroundArea() + Column( + modifier = + Modifier + .padding(top = statusBarHeightDp, bottom = navigationBarHeightDp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + // 뷰페이저 + HorizontalPager( + state = pagerState, + ) { page -> + OnboardingPageContent( + page = pages[page], + modifier = Modifier.fillMaxWidth(), + ) + } - Spacer(modifier = Modifier.size(25.dp)) + Spacer(modifier = Modifier.size(24.dp)) - // 페이지 인디케이터 - PageIndicator( - pageCount = pages.size, - currentPage = pagerState.currentPage, - ) + // 페이지 인디케이터 + PageIndicator( + pageCount = pages.size, + currentPage = pagerState.currentPage, + ) - Spacer(modifier = Modifier.size(32.dp)) - - // 다음/완료 버튼 - OnboardingButton( - currentPage = pagerState.currentPage, - totalPages = pages.size, - isLoading = uiState.isLoading, - onNextClick = { - if (pagerState.currentPage < pages.size - 1) { - scope.launch { - pagerState.animateScrollToPage(pagerState.currentPage + 1) - } - } else { - // 온보딩 완료 시 DataStore에 저장 - viewModel.completeOnboarding() - } - }, - ) - Spacer(modifier = Modifier.size(24.dp)) + Spacer(modifier = Modifier.size(14.dp)) + } + Column(modifier = Modifier.align(Alignment.BottomCenter)) { + // 다음/완료 버튼 + OnboardingButton( + currentPage = pagerState.currentPage, + totalPages = pages.size, + isLoading = uiState.isLoading, + onNextClick = { + if (pagerState.currentPage < pages.size - 1) { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + } else { + // 온보딩 완료 시 DataStore에 저장 + viewModel.completeOnboarding() + } + }, + ) + Spacer(modifier = Modifier.size(24.dp)) + } } } } @@ -165,7 +172,7 @@ private fun OnboardingPageContent( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(modifier = Modifier.size(45.dp)) + Spacer(modifier = Modifier.size(22.dp)) // 각 온보딩 페이지 타이틀 Text( @@ -178,15 +185,15 @@ private fun OnboardingPageContent( ), ) - Spacer(modifier = Modifier.size(24.dp)) + Spacer(modifier = Modifier.size(16.dp)) Image( - modifier = Modifier.weight(1f), + modifier = + Modifier + .clip(RoundedCornerShape(12.dp)), painter = painterResource(page.image), contentDescription = null, - contentScale = ContentScale.Crop, ) - } } @@ -217,7 +224,10 @@ private fun createAnnotatedText( /** * 지정된 색상으로 텍스트를 추가하는 함수 */ -private fun AnnotatedString.Builder.appendStyledText(text: String, color: Color) { +private fun AnnotatedString.Builder.appendStyledText( + text: String, + color: Color, +) { withStyle(style = SpanStyle(color = color)) { append(text) } From 17a5d8791d2dbc0bc95d2b79b70437f41f43d2ad Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 01:36:53 +0900 Subject: [PATCH 78/95] =?UTF-8?q?fix:=20login=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80,=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EA=B0=84=EA=B2=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/feature/login/LoginScreen.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt index cb786685..9c321d7e 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/login/LoginScreen.kt @@ -21,15 +21,19 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.repeatOnLifecycle +import coil.size.Scale import com.alarmy.near.R import com.alarmy.near.model.ProviderType import com.alarmy.near.presentation.feature.login.auth.SocialLoginHandler @@ -164,8 +168,9 @@ private fun LoginIntroductionSection(modifier: Modifier = Modifier) { ) Image( - modifier = modifier.wrapContentSize(Alignment.Center), + modifier = modifier.height(48.dp), alignment = Alignment.Center, + contentScale = ContentScale.None, painter = painterResource(R.drawable.ic_near_logo_title_primary), contentDescription = stringResource(R.string.near_logo_title), ) @@ -173,7 +178,8 @@ private fun LoginIntroductionSection(modifier: Modifier = Modifier) { Spacer(modifier = Modifier.size(LoginScreenConstants.DESCRIPTION_SPACING.dp)) Text( - modifier = modifier.wrapContentSize(Alignment.Center), + modifier = modifier, + textAlign = TextAlign.Center, text = stringResource(R.string.login_near_description), style = NearTheme.typography.B1_16_MEDIUM, color = NearTheme.colors.GRAY01_888888, @@ -228,3 +234,12 @@ private object LoginScreenConstants { const val DESCRIPTION_SPACING = 12 const val BOTTOM_SPACING = 96 } + +@Preview +@Composable +fun LoginScreenPreview() { + LoginScreen( + onLoginClick = {}, + uiState = LoginUiState(), + ) +} From 6d99aa7dafdaa6481e78e96d3f33143668a96734 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 01:41:05 +0900 Subject: [PATCH 79/95] =?UTF-8?q?fix:=20=EC=97=B0=EB=9D=BD=EC=B2=98=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=B2=84=ED=8A=BC=20=EC=83=89=EC=83=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NearSearchTextFieldDecorationBox.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt index 04a0a72e..c83affdd 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/internal/NearSearchTextFieldDecorationBox.kt @@ -33,34 +33,37 @@ internal fun NearSearchTextFieldDecorationBox( colors: TextFieldColors, placeHolderText: String, onSearchClick: () -> Unit, - contentPadding: PaddingValues = PaddingValues( - start = 16.dp, - top = 16.dp, - bottom = 16.dp, - end = 8.dp - ), + contentPadding: PaddingValues = + PaddingValues( + start = 16.dp, + top = 16.dp, + bottom = 16.dp, + end = 8.dp, + ), ) { OutlinedTextFieldDefaults.DecorationBox( contentPadding = contentPadding, value = value, innerTextField = { Box( - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) { Box( - modifier = Modifier - .fillMaxWidth() - .padding(end = 40.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(end = 40.dp), ) { innerTextField() } IconButton( onClick = onSearchClick, - modifier = Modifier - .align(Alignment.CenterEnd) - .size(32.dp), - enabled = enabled + modifier = + Modifier + .align(Alignment.CenterEnd) + .size(32.dp), + enabled = enabled, ) { Icon( painter = painterResource(id = R.drawable.ic_24_search), From 9501700aee3842adfd109da3f03149ed0f462da1 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 20:13:39 +0900 Subject: [PATCH 80/95] =?UTF-8?q?fix:=20My=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EB=B2=84=ED=8A=BC=20=ED=8C=A8=EB=94=A9=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 --- .../feature/myprofile/MyProfileScreen.kt | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/myprofile/MyProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/myprofile/MyProfileScreen.kt index 37f0092f..f62349e1 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/myprofile/MyProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/myprofile/MyProfileScreen.kt @@ -9,8 +9,11 @@ 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.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text @@ -129,6 +132,7 @@ fun MyProfileScreen( onWithdraw: () -> Unit = {}, onTermsClick: (TermsType) -> Unit = {}, ) { + val scrollState = rememberScrollState() NearFrame { // 앱바 NearTopAppbar( @@ -137,17 +141,21 @@ fun MyProfileScreen( onClickBackButton = onNavigateBack, ) - MyProfileInfoSection(uiState) - - // 일반 정보 섹션 Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), + modifier = Modifier.verticalScroll(scrollState), ) { - MyProfileGeneralSection(uiState) - MyProfileServiceInfoSection(onLogout, onWithdraw, onTermsClick) + MyProfileInfoSection(uiState) + + // 일반 정보 섹션 + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + ) { + MyProfileGeneralSection(uiState) + MyProfileServiceInfoSection(onLogout, onWithdraw, onTermsClick) + } } } } @@ -255,7 +263,7 @@ private fun ColumnScope.MyProfileServiceInfoSection( ) } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(63.dp)) NearLogoutButton( modifier = Modifier.fillMaxWidth(), From d091842da6dbf609abe83264e84bccc76ff56b6b Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 20:18:46 +0900 Subject: [PATCH 81/95] =?UTF-8?q?fix(NearOutlinedTextField):=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EC=8B=9C=20container=20=EC=83=89=EC=83=81=EC=9D=B4=20?= =?UTF-8?q?=EB=B3=80=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/component/textfield/NearOutlinedTextField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/NearOutlinedTextField.kt b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/NearOutlinedTextField.kt index 4b8e70a2..f159c151 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/NearOutlinedTextField.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/ui/component/textfield/NearOutlinedTextField.kt @@ -151,7 +151,7 @@ fun NearOutlinedTextField( container = { OutlinedTextFieldDefaults.Container( enabled = enabled, - isError = isError, + isError = false, interactionSource = interactionSource, colors = colors, shape = shape, From 97e372713baf0b3651263dee012aaa995483c618 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:01:24 +0900 Subject: [PATCH 82/95] =?UTF-8?q?fix(Friend):=20DateTime=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EB=A1=9C=EC=A7=81=EC=9D=84=20Data=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EC=97=90=EC=84=9C=20=EC=88=98=ED=96=89?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - preview에서 DateTime 계산으로 렌더링이 안되는 문제 개선 --- .../alarmy/near/data/mapper/FriendMapper.kt | 20 +++++++++++++++++++ .../main/java/com/alarmy/near/model/Friend.kt | 14 +++---------- .../friendprofile/FriendProfileScreen.kt | 16 ++------------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt index e797a6ae..940f6ff0 100644 --- a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt +++ b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt @@ -13,6 +13,9 @@ import com.alarmy.near.network.response.AnniversaryEntity import com.alarmy.near.network.response.ContactFrequencyEntity import com.alarmy.near.network.response.FriendEntity import com.alarmy.near.utils.logger.NearLog +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Locale fun FriendEntity.toModel(): Friend = Friend( @@ -25,9 +28,26 @@ fun FriendEntity.toModel(): Friend = anniversaryList = anniversaryList.map { it.toModel() }, memo = memo, phone = phone, + isContactToday = lastContactAt?.isToday() ?: false, lastContactAt = lastContactAt, + lastContactFormat = lastContactAt?.contactFormat() ) +fun String.contactFormat(): String { + val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val outputFormatter = DateTimeFormatter.ofPattern("M월 d일") + + val date = LocalDate.parse(this, inputFormatter) + return date.format(outputFormatter) +} + +private fun String.isToday(): Boolean { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.KOREA) + val targetDate = LocalDate.parse(this, formatter) + val today = LocalDate.now() + return targetDate == today +} + fun ContactFrequencyEntity.toModel(): ContactFrequency = ContactFrequency( reminderInterval = contactWeek.toReminderInterval(), diff --git a/Near/app/src/main/java/com/alarmy/near/model/Friend.kt b/Near/app/src/main/java/com/alarmy/near/model/Friend.kt index b37b23a6..ba1cfcf3 100644 --- a/Near/app/src/main/java/com/alarmy/near/model/Friend.kt +++ b/Near/app/src/main/java/com/alarmy/near/model/Friend.kt @@ -22,17 +22,9 @@ data class Friend( val memo: String?, val phone: String?, val lastContactAt: String?, // "2025-07-16" -) : Parcelable { - val isContactedToday: Boolean - get() = lastContactAt?.isToday() ?: false - - private fun String.isToday(): Boolean { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.KOREA) - val targetDate = LocalDate.parse(this, formatter) - val today = LocalDate.now() - return targetDate == today - } -} + val lastContactFormat: String? = null, // 친구 상세에 쓰이는 포맷 TODO 추후 UI 모델 분리 + val isContactToday: Boolean? = null, // 친구 상세에 쓰이는 포맷 TODO 추후 UI 모델 분리 +) : Parcelable @Serializable @Parcelize diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt index fc6fb64c..abe45613 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt @@ -22,9 +22,7 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Surface @@ -75,8 +73,6 @@ import com.alarmy.near.presentation.ui.extension.onNoRippleClick import com.alarmy.near.presentation.ui.theme.NearTheme import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.time.LocalDate -import java.time.format.DateTimeFormatter @Composable fun FriendProfileRoute( @@ -268,7 +264,7 @@ fun FriendProfileScreen( text = stringResource( R.string.friend_profile_last_contact_date_format, - friend.lastContactAt.lastContactFormat(), + friend.lastContactFormat ?: "", ), style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.BLUE01_5AA2E9, @@ -391,7 +387,7 @@ fun FriendProfileScreen( .padding(bottom = 24.dp) .align(Alignment.BottomCenter), contentPadding = PaddingValues(vertical = 17.dp), - enabled = friend.isContactedToday.not(), + enabled = friend.isContactToday?.not() ?: false, onClick = { onRecordFriendShip(friend.friendId) }, text = stringResource(R.string.friend_profile_record_button_text), ) @@ -665,14 +661,6 @@ fun Modifier.customTabIndicatorOffset( .width(currentTabWidth) } -private fun String.lastContactFormat(): String { - val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - val outputFormatter = DateTimeFormatter.ofPattern("M월 d일") - - val date = LocalDate.parse(this, inputFormatter) - return date.format(outputFormatter) -} - @Preview(showBackground = true) @Composable fun FriendProfileScreenPreview() { From d8b4276720dd8b364a6aa49e551d46504258ce0d Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:23:53 +0900 Subject: [PATCH 83/95] =?UTF-8?q?fix:=20FriendProfile=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20=EB=B2=94=EC=9C=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friendprofile/FriendProfileScreen.kt | 502 ++++++++++-------- 1 file changed, 288 insertions(+), 214 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt index abe45613..3e280e05 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.shape.RoundedCornerShape @@ -49,6 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -138,54 +140,55 @@ fun FriendProfileScreen( ) { val currentTabPosition = remember { mutableIntStateOf(0) } val dropdownState = remember { mutableStateOf(false) } -// val scrollState = rememberScrollState() NearFrame(modifier = modifier) { + if (recordSuccessDialogState) { + LaunchedEffect(true) { + if (recordSuccessDialogState) { + delay(2000L) + onDismissRecordSuccessDialog() + } + } + Dialog(onDismissRequest = onDismissRecordSuccessDialog) { + Column( + modifier = + Modifier + .width(255.dp) + .height(186.dp) + .background( + color = NearTheme.colors.WHITE_FFFFFF, + shape = RoundedCornerShape(16.dp), + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painterResource(R.drawable.img_100_character_success), + contentDescription = "", + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + stringResource(R.string.friend_profile_info_contact_success_text), + style = NearTheme.typography.B1_16_BOLD, + color = Color(0xff222222), + ) + } + } + } Box { when (friendState) { is FriendState.Success -> { val friend = friendState.friend + + // 전체를 세로로, 앱바는 고정, 나머지는 weight(1f) 로 영역 제한 Column( modifier = Modifier .align(Alignment.TopStart) .fillMaxSize() -// .verticalScroll(scrollState) .background(NearTheme.colors.WHITE_FFFFFF), ) { - if (recordSuccessDialogState) { - LaunchedEffect(true) { - if (recordSuccessDialogState) { - delay(2000L) - onDismissRecordSuccessDialog() - } - } - Dialog(onDismissRequest = onDismissRecordSuccessDialog) { - Column( - modifier = - Modifier - .width(255.dp) - .height(186.dp) - .background( - color = NearTheme.colors.WHITE_FFFFFF, - shape = RoundedCornerShape(16.dp), - ), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - painterResource(R.drawable.img_100_character_success), - contentDescription = "", - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - stringResource(R.string.friend_profile_info_contact_success_text), - style = NearTheme.typography.B1_16_BOLD, - color = Color(0xff222222), - ) - } - } - } + // (1) 상단 AppBar — 고정 NearTopAppbar( title = stringResource(R.string.friend_profile_title), onClickBackButton = onClickBackButton, @@ -193,10 +196,9 @@ fun FriendProfileScreen( Column(modifier = Modifier.padding(end = 20.dp)) { Image( modifier = - Modifier - .onNoRippleClick(onClick = { - dropdownState.value = true - }), + Modifier.onNoRippleClick(onClick = { + dropdownState.value = true + }), painter = painterResource(R.drawable.ic_32_menu), contentDescription = stringResource(R.string.common_menu_button_description), ) @@ -223,174 +225,209 @@ fun FriendProfileScreen( }, ) - Spacer(modifier = Modifier.height(18.dp)) - Row( + // (2) 나머지 영역을 제한된 높이로 만들기 -> 이 안에서 스크롤 가능 + Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 32.dp), - verticalAlignment = Alignment.CenterVertically, + .weight(1f), // <--- 요게 중요: 여기서 높이가 제한되어 내부 스크롤이 가능해짐 ) { - Box( - modifier = - Modifier, + // 컨텐츠는 스크롤 가능하거나 Lazy로 구성 + // 여기서는 상단의 프로필 요약(이미지/이름/버튼 등)을 스크롤 헤더로 포함시키고 + // 탭에 따라 ProfileTab(스크롤 필요 없음) 또는 RecordTab(LazyVerticalGrid)를 표시 + // 상단 요약을 포함한 전체를 LazyColumn으로 만들면 header + list 형태로 자연스럽게 동작 + + LazyColumn( + modifier = Modifier.fillMaxSize(), ) { - Image( - modifier = Modifier.align(Alignment.Center), - painter = painterResource(R.drawable.img_80_user1), - contentDescription = null, - ) - Image( - modifier = - Modifier - .align(Alignment.TopEnd) - .offset(x = 2.dp, y = (-2).dp), - painter = painterResource(R.drawable.ic_visual_24_emoji_0), - contentDescription = null, - ) - } - Spacer(modifier = Modifier.width(24.dp)) - Column { - Text( - modifier = Modifier.widthIn(max = 145.dp), - text = friend.name, - style = NearTheme.typography.B1_16_BOLD, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - if (friend.lastContactAt != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = - stringResource( - R.string.friend_profile_last_contact_date_format, - friend.lastContactFormat ?: "", - ), - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.BLUE01_5AA2E9, + item { + Spacer(modifier = Modifier.height(18.dp)) + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box { + // 이미지 + 이모지 + Image( + modifier = Modifier.align(Alignment.Center), + painter = painterResource(R.drawable.img_80_user1), + contentDescription = null, + ) + Image( + modifier = + Modifier + .align(Alignment.TopEnd) + .offset(x = 2.dp, y = (-2).dp), + painter = painterResource(R.drawable.ic_visual_24_emoji_0), + contentDescription = null, + ) + } + Spacer(modifier = Modifier.width(24.dp)) + Column { + Text( + modifier = Modifier.widthIn(max = 145.dp), + text = friend.name, + style = NearTheme.typography.B1_16_BOLD, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + if (friend.lastContactAt != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = + stringResource( + R.string.friend_profile_last_contact_date_format, + friend.lastContactFormat ?: "", + ), + style = NearTheme.typography.B2_14_MEDIUM, + color = NearTheme.colors.BLUE01_5AA2E9, + ) + } + } + } + Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) { + CallButton( + modifier = Modifier.weight(1f), + enabled = !friend.phone.isNullOrBlank(), + onClick = { + friend.phone?.let { + onClickCallButton(friend.phone) + } + }, + ) + Spacer(modifier = Modifier.width(7.dp)) + MessageButton( + Modifier.weight(1f), + enabled = !friend.phone.isNullOrBlank(), + onClick = { + friend.phone?.let { + onClickMessageButton(friend.phone) + } + }, + ) + } + Spacer(modifier = Modifier.height(24.dp)) + // TabRow + TabRow( + modifier = + Modifier + .padding(horizontal = 25.dp) + .width(170.dp), + containerColor = NearTheme.colors.WHITE_FFFFFF, + selectedTabIndex = currentTabPosition.intValue, + divider = {}, + indicator = { + TabRowDefaults.SecondaryIndicator( + modifier = + Modifier.customTabIndicatorOffset( + it[currentTabPosition.intValue], + 80.dp, + ), + height = 3.dp, + color = NearTheme.colors.BLUE01_5AA2E9, + ) + }, + ) { + Tab( + modifier = + Modifier + .width(85.dp) + .height(50.dp), + selected = currentTabPosition.intValue == 0, + onClick = { currentTabPosition.intValue = 0 }, + ) { + Text( + text = stringResource(R.string.friend_profile_tab_text_profile), + style = + if (currentTabPosition.intValue == + 0 + ) { + NearTheme.typography.B2_14_BOLD + } else { + NearTheme.typography.B2_14_MEDIUM + }, + color = + if (currentTabPosition.intValue == + 0 + ) { + NearTheme.colors.BLACK_1A1A1A + } else { + NearTheme.colors.GRAY02_B7B7B7 + }, + ) + } + Tab( + modifier = + Modifier + .width(85.dp) + .height(50.dp), + selected = currentTabPosition.intValue == 1, + onClick = { currentTabPosition.intValue = 1 }, + ) { + Text( + text = stringResource(R.string.friend_profile_tab_text_record), + style = + if (currentTabPosition.intValue == + 1 + ) { + NearTheme.typography.B2_14_BOLD + } else { + NearTheme.typography.B2_14_MEDIUM + }, + color = + if (currentTabPosition.intValue == + 1 + ) { + NearTheme.colors.BLACK_1A1A1A + } else { + NearTheme.colors.GRAY02_B7B7B7 + }, + ) + } + } + HorizontalDivider( + thickness = 1.dp, + color = NearTheme.colors.GRAY03_EBEBEB, ) + } // end header item + + // 탭별 본문을 lazy column의 item으로 넣음 — 이렇게 하면 중첩 스크롤 문제 회피 + item { + if (currentTabPosition.intValue == 0) { + // ProfileTab : 내부에 스크롤을 또 만들 필요 없음. LazyColumn이 전체를 스크롤함. + ProfileTab(friend = friend) + } else { + RecordTab( + friendShipRecordState = friendShipRecordState, + ) + } } - } - } - Spacer(modifier = Modifier.height(24.dp)) - Row( + item { Spacer(modifier = Modifier.height(80.dp)) } // 하단 여유 + } // end LazyColumn + } // end weighted Column + + // (3) 하단 고정 버튼 + NearSolidTypeButton( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp), - ) { - CallButton( - modifier = Modifier.weight(1f), - enabled = !friend.phone.isNullOrBlank(), - onClick = { - friend.phone?.let { - onClickCallButton(friend.phone) - } - }, - ) - Spacer(modifier = Modifier.width(7.dp)) - MessageButton( - Modifier.weight(1f), - enabled = !friend.phone.isNullOrBlank(), - onClick = { - friend.phone?.let { - onClickMessageButton(friend.phone) - } - }, - ) - } - Spacer(modifier = Modifier.height(24.dp)) - TabRow( - modifier = - Modifier - .padding(horizontal = 25.dp) - .width(170.dp), - containerColor = NearTheme.colors.WHITE_FFFFFF, - selectedTabIndex = 0, - divider = {}, - indicator = { - TabRowDefaults.SecondaryIndicator( - modifier = - Modifier - .customTabIndicatorOffset( - it[currentTabPosition.intValue], - 80.dp, - ), // 넓이, 애니메이션 지정 - // 모양 지정 - height = 3.dp, - color = NearTheme.colors.BLUE01_5AA2E9, - ) - }, - ) { - Tab( - modifier = - Modifier - .width(85.dp) - .height(50.dp), - selected = true, - onClick = { - currentTabPosition.intValue = 0 - }, - ) { - if (currentTabPosition.intValue == 0) { - Text( - text = stringResource(R.string.friend_profile_tab_text_profile), - style = NearTheme.typography.B2_14_BOLD, - color = NearTheme.colors.BLACK_1A1A1A, - ) - } else { - Text( - text = stringResource(R.string.friend_profile_tab_text_profile), - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.GRAY02_B7B7B7, - ) - } - } - Tab( - modifier = - Modifier - .width(85.dp) - .height(50.dp), - selected = true, - onClick = { - currentTabPosition.intValue = 1 - }, - ) { - if (currentTabPosition.intValue == 1) { - Text( - text = stringResource(R.string.friend_profile_tab_text_record), - style = NearTheme.typography.B2_14_BOLD, - color = NearTheme.colors.BLACK_1A1A1A, - ) - } else { - Text( - text = stringResource(R.string.friend_profile_tab_text_record), - style = NearTheme.typography.B2_14_MEDIUM, - color = NearTheme.colors.GRAY02_B7B7B7, - ) - } - } - } - HorizontalDivider(thickness = 1.dp, color = NearTheme.colors.GRAY03_EBEBEB) - if (currentTabPosition.intValue == 0) { - ProfileTab(friend = friend) - } else { - RecordTab(friendShipRecordState = friendShipRecordState) - } - Spacer(modifier = Modifier.height(60.dp)) - } - NearSolidTypeButton( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .padding(bottom = 24.dp) - .align(Alignment.BottomCenter), - contentPadding = PaddingValues(vertical = 17.dp), - enabled = friend.isContactToday?.not() ?: false, - onClick = { onRecordFriendShip(friend.friendId) }, - text = stringResource(R.string.friend_profile_record_button_text), - ) + .padding(horizontal = 20.dp) + .padding(bottom = 24.dp), + contentPadding = PaddingValues(vertical = 17.dp), + enabled = friend.isContactToday?.not() ?: false, + onClick = { onRecordFriendShip(friend.friendId) }, + text = stringResource(R.string.friend_profile_record_button_text), + ) + } // end parent Column } is FriendState.Loading -> { @@ -407,9 +444,9 @@ fun FriendProfileScreen( ) } } - } - } - } + } // end when + } // end Box + } // end NearFrame } @Composable @@ -454,16 +491,28 @@ private fun RecordTab( modifier: Modifier = Modifier, friendShipRecordState: FriendShipRecordState, ) { - Column(modifier = modifier.padding(horizontal = 24.dp)) { + Column( + modifier = + modifier + .fillMaxWidth() // 부모 스크롤(LazyColumn)에 의존하도록 fillMaxWidth만 사용 + .padding(horizontal = 24.dp), + ) { Spacer(modifier = Modifier.height(24.dp)) Text( stringResource(R.string.friend_profile_info_record_title_text), style = NearTheme.typography.B2_14_BOLD, color = NearTheme.colors.BLACK_1A1A1A, ) + if (friendShipRecordState.isEmpty) { - Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) { - Spacer(modifier = Modifier.height(60.dp)) + // 빈 상태는 중앙 정렬이지만 높이를 무한으로 잡지 않도록 fillMaxWidth 사용 + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 60.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { Image(painterResource(R.drawable.img_100_character_empty), contentDescription = null) Spacer(modifier = Modifier.height(16.dp)) Text( @@ -474,19 +523,44 @@ private fun RecordTab( } } else { Spacer(modifier = Modifier.height(13.dp)) - LazyVerticalGrid( - columns = GridCells.Fixed(3), - verticalArrangement = Arrangement.spacedBy(24.dp), - contentPadding = PaddingValues(bottom = 60.dp), - ) { - items(friendShipRecordState.records.size) { - RecordItem( - friendRecord = friendShipRecordState.records[friendShipRecordState.records.size - 1 - it], - index = - friendShipRecordState.records.size - it, - ) + + // records를 3개씩 묶어서 행(Row)으로 렌더 — 자체 스크롤 없음 + val rows = friendShipRecordState.records.chunked(3) + Column(modifier = Modifier.fillMaxWidth()) { + rows.forEach { rowItems -> + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + // 아이템 간 세로 간격 + horizontalArrangement = Arrangement.spacedBy(24.dp), + ) { + // 각 row의 아이템을 동일한 폭으로 나눔 + rowItems.forEachIndexed { index, item -> + // 인덱스나 번호 표시 로직은 기존과 동일하게 계산 + val globalIndex = + friendShipRecordState.records.size - (rows.indexOf(rowItems) * 3 + index) + Box(modifier = Modifier.weight(1f)) { + RecordItem( + modifier = Modifier.wrapContentSize(), + index = globalIndex, + friendRecord = item, + ) + } + } + + // 만약 마지막 행에 3 미만의 아이템이 있으면 빈 공간을 채워 균형 맞춤 + if (rowItems.size < 3) { + repeat(3 - rowItems.size) { + Spacer(modifier = Modifier.weight(1f)) + } + } + } } } + + Spacer(modifier = Modifier.height(24.dp)) } } } From 5890721d247a491441ce8f5e312886c6cc3786a0 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:34:41 +0900 Subject: [PATCH 84/95] =?UTF-8?q?feat:=20=EC=B1=99=EA=B9=80=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=ED=95=98=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EA=B7=B8?= =?UTF-8?q?=EB=9D=BC=EB=8D=B0=EC=9D=B4=EC=85=98=20=EB=B0=B0=EA=B2=BD=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friendprofile/FriendProfileScreen.kt | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt index 3e280e05..22af9929 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt @@ -41,6 +41,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.res.painterResource @@ -416,17 +418,29 @@ fun FriendProfileScreen( } // end weighted Column // (3) 하단 고정 버튼 - NearSolidTypeButton( + Box( modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - .padding(bottom = 24.dp), - contentPadding = PaddingValues(vertical = 17.dp), - enabled = friend.isContactToday?.not() ?: false, - onClick = { onRecordFriendShip(friend.friendId) }, - text = stringResource(R.string.friend_profile_record_button_text), - ) + Modifier.fillMaxWidth().background( + brush = + Brush.linearGradient( + colors = listOf(Color(0x00FFFFFF), Color(0xFFFFFFFF)), // 파랑 → 밝은 하늘색 + start = Offset(0f, 0f), // 위쪽 시작 + end = Offset(0f, Float.POSITIVE_INFINITY) + ), + ), + ) { + NearSolidTypeButton( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(bottom = 24.dp, top = 16.dp), + contentPadding = PaddingValues(vertical = 17.dp), + enabled = friend.isContactToday?.not() ?: false, + onClick = { onRecordFriendShip(friend.friendId) }, + text = stringResource(R.string.friend_profile_record_button_text), + ) + } } // end parent Column } From cf86fcd822000b1bc2451ae45be8aa192c04a532 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:36:45 +0900 Subject: [PATCH 85/95] =?UTF-8?q?fix:=20=EC=B9=9C=EA=B5=AC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=20=EB=A9=94=EB=AA=A8?= =?UTF-8?q?=EC=99=80=20BasicText=EA=B0=84=20=EA=B0=84=EA=B2=A9=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 --- .../feature/friendprofileedittor/FriendProfileEditorScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt index aab62b35..ddff2c75 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorScreen.kt @@ -576,7 +576,7 @@ fun FriendProfileEditorScreen( style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.GRAY01_888888, ) - Spacer(modifier = Modifier.width(23.dp)) + Spacer(modifier = Modifier.width(62.dp)) NearLimitedTextField( modifier = Modifier From 01694c5f58e8ea9beb2d69c5be2c2345904a52f2 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:44:12 +0900 Subject: [PATCH 86/95] =?UTF-8?q?fix:=20UI=20=ED=91=9C=EC=8B=9C=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=82=A0=EC=A7=9C=20=ED=8F=AC=EB=A7=B7=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 --- .../presentation/feature/friendprofile/FriendProfileScreen.kt | 2 +- .../friendprofileedittor/uistate/FriendProfileEditorUIState.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt index 22af9929..16d2a3a3 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt @@ -489,7 +489,7 @@ private fun ProfileTab( category = stringResource(R.string.friend_profile_info_category_anniversary), content = friend.anniversaryList.joinToString(" ") { - "${it.title} (${it.date})" + "${it.title} (${it.date?.replace("-",".")})" }, ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt index 4e92060a..ea436d3f 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt @@ -52,7 +52,7 @@ fun FriendProfileEditorUIState.toModel( relation = relation, contactFrequency = contactFrequency, - birthday = birthday.value?.replace(".", "-"), + birthday = birthday.value?.replace(".", "."), anniversaryList = anniversaries.map { Anniversary( From c9ef039de400a212cddda01807b441a659cc8a55 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:45:56 +0900 Subject: [PATCH 87/95] =?UTF-8?q?fix:=20=EC=A0=9C=ED=95=9C=20=EA=B8=80?= =?UTF-8?q?=EC=9E=90=20=EC=88=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friendprofileedittor/FriendProfileEditorViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt index 8892075e..10be95b3 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/FriendProfileEditorViewModel.kt @@ -202,6 +202,6 @@ class FriendProfileEditorViewModel companion object { private const val MAX_NAME_LENGTH = 20 - private const val MAX_MEMO_LENGTH = 200 + private const val MAX_MEMO_LENGTH = 100 } } From 8e9316a54f8fc9c2a26c81352c5eb66806ffca77 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 11 Nov 2025 21:46:12 +0900 Subject: [PATCH 88/95] =?UTF-8?q?fix:=20=ED=99=88=20lazyRow=20=ED=82=A4=20?= =?UTF-8?q?=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/alarmy/near/presentation/feature/home/HomeScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 129204f9..8580d4e1 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -232,7 +232,7 @@ internal fun HomeScreen( items( count = monthlyFriends.size, key = { - monthlyFriends[it].friendId + "monthly_friend_${monthlyFriends[it].friendId}" }, ) { val monthlyContact = monthlyFriends[it] From c14476ac578eabe8662b5b3bd16ab92caf20ba50 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Wed, 12 Nov 2025 18:39:00 +0900 Subject: [PATCH 89/95] =?UTF-8?q?fix:=20=EC=B9=9C=EA=B5=AC=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=EC=8B=9C=20=EC=83=9D=EC=9D=BC=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt | 4 ++-- .../uistate/FriendProfileEditorUIState.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt index 940f6ff0..b11dc98f 100644 --- a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt +++ b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt @@ -30,7 +30,7 @@ fun FriendEntity.toModel(): Friend = phone = phone, isContactToday = lastContactAt?.isToday() ?: false, lastContactAt = lastContactAt, - lastContactFormat = lastContactAt?.contactFormat() + lastContactFormat = lastContactAt?.contactFormat(), ) fun String.contactFormat(): String { @@ -66,7 +66,7 @@ fun Friend.toRequest(): FriendRequest = name = name, relation = relation.toString(), contactFrequency = contactFrequency.toRequest(), - birthday = birthday, + birthday = birthday?.replace(".", "-"), anniversaryList = anniversaryList.map { it.toRequest() }, memo = memo, phone = phone, diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt index ea436d3f..45b423ce 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofileedittor/uistate/FriendProfileEditorUIState.kt @@ -52,7 +52,7 @@ fun FriendProfileEditorUIState.toModel( relation = relation, contactFrequency = contactFrequency, - birthday = birthday.value?.replace(".", "."), + birthday = birthday.value?.replace("-", "."), anniversaryList = anniversaries.map { Anniversary( From 557151824f99f934523110d65a0e388dced177f5 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 13 Nov 2025 00:01:34 +0900 Subject: [PATCH 90/95] =?UTF-8?q?fix:=20=EC=B9=9C=EA=B5=AC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=8B=9C=20=EC=9D=B4=EB=B2=88=EB=8B=AC=20=EC=B1=99?= =?UTF-8?q?=EA=B8=B8=20=EC=82=AC=EB=9E=8C=EB=8F=84=20=EC=A0=9C=EC=99=B8?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../near/presentation/feature/home/HomeViewModel.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt index c52e39c8..96b35ec4 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeViewModel.kt @@ -9,7 +9,6 @@ import com.alarmy.near.presentation.feature.home.model.MonthlyFriendUIState import com.alarmy.near.presentation.feature.home.model.MyFriendUIState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch @@ -68,10 +67,14 @@ class HomeViewModel } } launch { - friendRepository - .fetchMonthlyFriends() - .catch { - _errorEvent.send(it) + combine( + friendRepository + .fetchMonthlyFriends(), + deletedFriendIdsFlow, + ) { monthlyFriends, deletedIds -> + monthlyFriends.filter { it.friendId !in deletedIds } + }.catch { + _errorEvent.send(it) }.collect { _uiState.update { state -> state.copy( From ccdd668ad1e1632a5d8a982d0e7548b5e5fc799a Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Thu, 13 Nov 2025 00:15:54 +0900 Subject: [PATCH 91/95] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A6=AC=ED=8C=85=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=A0=95=EB=A0=AC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/feature/home/HomeScreen.kt | 100 +++++++++--------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 8580d4e1..3fa9edc4 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -127,48 +128,47 @@ internal fun HomeScreen( .fillMaxSize(), ) { Spacer(modifier = Modifier.height(statusBarHeightDp)) - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - .padding(end = 20.dp), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.onNoRippleClick(onClick = onMyPageClick), - text = stringResource(R.string.home_my_profile_button_text), - style = NearTheme.typography.H2_18_BOLD.copy(letterSpacing = 0.sp), - color = NearTheme.colors.WHITE_FFFFFF, - ) - Spacer(modifier = Modifier.width(12.dp)) - Image( - modifier = Modifier.onNoRippleClick(onClick = onAlarmClick), - painter = painterResource(R.drawable.ic_32_bell), - contentDescription = "", - ) - } - Spacer(modifier = Modifier.height(16.dp)) - - Box(modifier = Modifier.padding(horizontal = 24.dp)) { - NearConditionalShimmer(enabled = uiState.memberInfo == null) { + Box(modifier = Modifier.weight(1f)) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .padding(end = 20.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { Text( - text = - buildAnnotatedString { - append("${uiState.memberInfo?.nickname}님,\n") - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append("누구를 챙길지") - } - append(" 정해볼까요?") - }, - style = NearTheme.typography.H1_24_REGULAR, + modifier = Modifier.onNoRippleClick(onClick = onMyPageClick), + text = stringResource(R.string.home_my_profile_button_text), + style = NearTheme.typography.H2_18_BOLD.copy(letterSpacing = 0.sp), color = NearTheme.colors.WHITE_FFFFFF, ) + Spacer(modifier = Modifier.width(12.dp)) + Image( + modifier = Modifier.onNoRippleClick(onClick = onAlarmClick), + painter = painterResource(R.drawable.ic_32_bell), + contentDescription = "", + ) } - } - Spacer(modifier = Modifier.height(32.dp)) + Box(modifier = Modifier.padding(horizontal = 24.dp).align(Alignment.CenterStart)) { + NearConditionalShimmer(enabled = uiState.memberInfo == null) { + Text( + text = + buildAnnotatedString { + append("${uiState.memberInfo?.nickname}님,\n") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append("누구를 챙길지") + } + append(" 정해볼까요?") + }, + style = NearTheme.typography.H1_24_REGULAR, + color = NearTheme.colors.WHITE_FFFFFF, + ) + } + } + } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -329,7 +329,8 @@ private fun MyFriends( Box( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() + .height(490.dp) .background( color = NearTheme.colors.WHITE_FFFFFF, shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), @@ -339,7 +340,8 @@ private fun MyFriends( Column( modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally) { + horizontalAlignment = Alignment.CenterHorizontally, + ) { Spacer(modifier = Modifier.height(166.dp)) NearConditionalShimmer( shimmerColors = ShimmerType.WHITE.colors, @@ -476,7 +478,7 @@ internal fun HomeScreenPreview() { HomeUiState( myFriendUIState = MyFriendUIState.Success( - List(5) { + List(7) { FriendSummary( id = "2003", name = "일이삼사오육칠팔구", @@ -488,14 +490,16 @@ internal fun HomeScreenPreview() { }, ), monthlyFriendUIState = - MonthlyFriendUIState.Success(List(4) { - MonthlyFriend( - friendId = "intellegat$it", - name = "Stacey Stewart", - type = MonthlyFriendType.ANNIVERSARY, - nextContactAt = "2025-09-30", - ) - },), + MonthlyFriendUIState.Success( + List(4) { + MonthlyFriend( + friendId = "intellegat$it", + name = "Stacey Stewart", + type = MonthlyFriendType.ANNIVERSARY, + nextContactAt = "2025-09-30", + ) + }, + ), memberInfo = MemberInfo( memberId = "posidonium", From 7fcf4f5456d8631416b56ba778355d506adf0f89 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Mon, 17 Nov 2025 23:26:58 +0900 Subject: [PATCH 92/95] =?UTF-8?q?feat:=20monthlyFriend=ED=82=A4=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/alarmy/near/presentation/feature/home/HomeScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt index 3fa9edc4..3e4a06fc 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/home/HomeScreen.kt @@ -231,9 +231,9 @@ internal fun HomeScreen( ) { items( count = monthlyFriends.size, - key = { - "monthly_friend_${monthlyFriends[it].friendId}" - }, +// key = { +// "monthly_friend_${monthlyFriends[it].friendId}" +// }, ) { val monthlyContact = monthlyFriends[it] val now = LocalDate.now() From 24d275f5e1f115c0b3ec2b4ba6f7f79d3fc128c7 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Mon, 17 Nov 2025 23:34:27 +0900 Subject: [PATCH 93/95] =?UTF-8?q?fix:=20=EC=B1=99=EA=B9=80=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=EC=9D=B4=20=EC=A0=81=EC=9A=A9=EC=95=88=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alarmy/near/data/mapper/FriendMapper.kt | 2 +- .../friendprofile/FriendProfileViewModel.kt | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt index b11dc98f..04f91912 100644 --- a/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt +++ b/Near/app/src/main/java/com/alarmy/near/data/mapper/FriendMapper.kt @@ -28,8 +28,8 @@ fun FriendEntity.toModel(): Friend = anniversaryList = anniversaryList.map { it.toModel() }, memo = memo, phone = phone, - isContactToday = lastContactAt?.isToday() ?: false, lastContactAt = lastContactAt, + isContactToday = lastContactAt?.isToday() ?: false, lastContactFormat = lastContactAt?.contactFormat(), ) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt index 4a766269..90926c87 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileViewModel.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.Locale @@ -118,6 +117,10 @@ class FriendProfileViewModel friend = it.friend.copy( lastContactAt = getTodayDashFormat(), + lastContactFormat = it.friend.lastContactAt?.contactFormat(), + isContactToday = + it.friend.lastContactAt?.isToday() + ?: false, ), ) } @@ -146,4 +149,19 @@ class FriendProfileViewModel _friendFlow.update { (it as FriendState.Success).copy(friend = friend) } } } + + fun String.contactFormat(): String { + val inputFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val outputFormatter = DateTimeFormatter.ofPattern("M월 d일") + + val date = LocalDate.parse(this, inputFormatter) + return date.format(outputFormatter) + } + + private fun String.isToday(): Boolean { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.KOREA) + val targetDate = LocalDate.parse(this, formatter) + val today = LocalDate.now() + return targetDate == today } +} From c02630c6a1cdf9cf1bf48fb2c735852cba5ec375 Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Mon, 17 Nov 2025 23:57:03 +0900 Subject: [PATCH 94/95] =?UTF-8?q?fix:=20=EB=A6=AC=ED=94=8C=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../friendprofile/FriendProfileScreen.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt index 16d2a3a3..9ca3720d 100644 --- a/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt +++ b/Near/app/src/main/java/com/alarmy/near/presentation/feature/friendprofile/FriendProfileScreen.kt @@ -21,8 +21,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider @@ -52,7 +50,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.times import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -344,7 +341,11 @@ fun FriendProfileScreen( Modifier .width(85.dp) .height(50.dp), - selected = currentTabPosition.intValue == 0, + selectedContentColor = + NearTheme.colors.GRAY01_888888.copy( + alpha = 0.3f, + ), + selected = currentTabPosition.intValue == 0, onClick = { currentTabPosition.intValue = 0 }, ) { Text( @@ -374,6 +375,9 @@ fun FriendProfileScreen( .height(50.dp), selected = currentTabPosition.intValue == 1, onClick = { currentTabPosition.intValue = 1 }, + selectedContentColor = NearTheme.colors.GRAY01_888888.copy( + alpha = 0.3f + ), ) { Text( text = stringResource(R.string.friend_profile_tab_text_record), @@ -424,8 +428,8 @@ fun FriendProfileScreen( brush = Brush.linearGradient( colors = listOf(Color(0x00FFFFFF), Color(0xFFFFFFFF)), // 파랑 → 밝은 하늘색 - start = Offset(0f, 0f), // 위쪽 시작 - end = Offset(0f, Float.POSITIVE_INFINITY) + start = Offset(0f, 0f), // 위쪽 시작 + end = Offset(0f, Float.POSITIVE_INFINITY), ), ), ) { @@ -614,7 +618,8 @@ private fun RecordItem( contentDescription = null, ) Text( - stringResource(R.string.friend_profile_info_contact_record_text, index), + text = stringResource(R.string.friend_profile_info_contact_record_text, index), + textAlign = TextAlign.Center, style = NearTheme.typography.B2_14_MEDIUM, color = NearTheme.colors.BLUE01_5AA2E9, ) From baebe46bddbe79a87cc14b27cb0afca9d54112dc Mon Sep 17 00:00:00 2001 From: kwakjoohyeong Date: Tue, 18 Nov 2025 22:08:42 +0900 Subject: [PATCH 95/95] =?UTF-8?q?chore:=20=EB=B2=84=EC=A0=84=20=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Near/app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Near/app/build.gradle.kts b/Near/app/build.gradle.kts index 047d1968..c46d38da 100644 --- a/Near/app/build.gradle.kts +++ b/Near/app/build.gradle.kts @@ -18,7 +18,7 @@ android { applicationId = "com.alarmy.near" minSdk = 27 targetSdk = 35 - versionCode = 3 + versionCode = 4 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"