From 499916e44627fa437ed73b222f27715cfb8ebfff Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:04:34 +0900 Subject: [PATCH 001/299] =?UTF-8?q?[MOD/#260]=20ClodyApp=20->=20ClodyAppli?= =?UTF-8?q?cation=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=A6=84=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 --- app/src/main/AndroidManifest.xml | 2 +- .../java/com/sopt/clody/{ClodyApp.kt => ClodyApplication.kt} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/src/main/java/com/sopt/clody/{ClodyApp.kt => ClodyApplication.kt} (93%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ff9e0c90..e3e223b2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,7 +8,7 @@ Date: Wed, 7 May 2025 01:05:21 +0900 Subject: [PATCH 002/299] =?UTF-8?q?[ADD/#260]=20=EC=95=B1=20=EB=82=B4?= =?UTF-8?q?=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EB=A5=BC=20sealed=20interface=EB=A1=9C=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=ED=95=98=EC=97=AC=20type-safe=ED=95=9C=20=EC=9D=B8=EC=9E=90=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=20=EA=B5=AC=EC=A1=B0=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/utils/navigation/Route.kt | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt new file mode 100644 index 00000000..4ae1aa8f --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt @@ -0,0 +1,85 @@ +package com.sopt.clody.presentation.utils.navigation + +import kotlinx.serialization.Serializable + +/** + * sealed interface로 정의한 앱 내 네비게이션 Route + * @Serializable을 활용해 arguments를 type-safe하게 전달. + */ + +@Serializable +sealed interface Route { + + @Serializable + data object Splash : Route + + @Serializable + data object Login : Route + + @Serializable + data object TermsOfService : Route + + @Serializable + data object Nickname : Route + + @Serializable + data object TimeReminder : Route + + @Serializable + data object Guide : Route + + @Serializable + data class Home( + val selectedYear: Int, + val selectedMonth: Int, + val selectedDay: Int? = null, + ) : Route + + @Serializable + data class DiaryList( + val selectedYearFromHome: Int, + val selectedMonthFromHome: Int, + ) : Route + + @Serializable + data class WriteDiary( + val year: Int, + val month: Int, + val date: Int, + ) : Route + + @Serializable + data class ReplyLoading( + val year: Int, + val month: Int, + val date: Int, + val from: ReplyLoadingFrom = ReplyLoadingFrom.HOME, + val replyStatus: ReplyStatus = ReplyStatus.UNREADY, + ) : Route { + @Serializable + enum class ReplyLoadingFrom { + HOME, + DIARY_LIST, + } + } + + @Serializable + data class ReplyDiary( + val year: Int, + val month: Int, + val date: Int, + val replyStatus: ReplyStatus = ReplyStatus.UNREADY, + ) : Route + + @Serializable + data object Setting : Route + + @Serializable + data object AccountManagement : Route + + @Serializable + data object NotificationSetting : Route + + @Serializable + data class WebView(val encodedUrl: String) : Route +} From 00be1288557461bcc3b127804ba13ccd0922e79d Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:05:58 +0900 Subject: [PATCH 003/299] =?UTF-8?q?[ADD/#260]=20safePopBackStack=20?= =?UTF-8?q?=ED=97=AC=ED=8D=BC=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/navigation/NavControllerExtensions.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/navigation/NavControllerExtensions.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/NavControllerExtensions.kt b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/NavControllerExtensions.kt new file mode 100644 index 00000000..982bbdd9 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/NavControllerExtensions.kt @@ -0,0 +1,12 @@ +package com.sopt.clody.presentation.utils.navigation + +import androidx.navigation.NavController + +/** + * 현재 backStack 상단에서 popBackStack 동작을 수행하는 helper 함수. + */ +fun NavController.safePopBackStack() { + if (currentBackStackEntry?.lifecycle?.currentState == androidx.lifecycle.Lifecycle.State.RESUMED) { + popBackStack() + } +} From 4fdf85b725dc5420be42bee37eb139fb7b8bfc51 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:07:45 +0900 Subject: [PATCH 004/299] =?UTF-8?q?[ADD/#260]=20ClodyNavHost=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EC=A0=80=EB=B8=94=20=EC=B6=94=EA=B0=80(=EC=95=B1=20?= =?UTF-8?q?=EC=A0=84=EC=97=AD=20=EB=82=B4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/main/ClodyNavHost.kt | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt new file mode 100644 index 00000000..a8c43e60 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt @@ -0,0 +1,130 @@ +package com.sopt.clody.presentation.ui.main + +import android.content.Intent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import com.sopt.clody.presentation.ui.auth.guide.guideScreen +import com.sopt.clody.presentation.ui.auth.guide.navigateToGuide +import com.sopt.clody.presentation.ui.auth.login.navigation.loginScreen +import com.sopt.clody.presentation.ui.auth.login.navigation.navigateToLogin +import com.sopt.clody.presentation.ui.auth.signup.navigation.navigateToNickname +import com.sopt.clody.presentation.ui.auth.signup.navigation.navigateToTermsOfService +import com.sopt.clody.presentation.ui.auth.signup.navigation.nicknameScreen +import com.sopt.clody.presentation.ui.auth.signup.navigation.termsOfServiceScreen +import com.sopt.clody.presentation.ui.auth.timereminder.navigateToTimeReminder +import com.sopt.clody.presentation.ui.auth.timereminder.timeReminderScreen +import com.sopt.clody.presentation.ui.diarylist.navigation.diaryListScreen +import com.sopt.clody.presentation.ui.diarylist.navigation.navigateToDiaryList +import com.sopt.clody.presentation.ui.home.navigation.homeScreen +import com.sopt.clody.presentation.ui.home.navigation.navigateToHome +import com.sopt.clody.presentation.ui.replydiary.navigation.navigateToReplyDiary +import com.sopt.clody.presentation.ui.replydiary.navigation.replyDiaryScreen +import com.sopt.clody.presentation.ui.replyloading.navigation.navigateToReplyLoading +import com.sopt.clody.presentation.ui.replyloading.navigation.replyLoadingScreen +import com.sopt.clody.presentation.ui.setting.navigation.accountManagementScreen +import com.sopt.clody.presentation.ui.setting.navigation.navigateToAccountManagement +import com.sopt.clody.presentation.ui.setting.navigation.navigateToNotificationSetting +import com.sopt.clody.presentation.ui.setting.navigation.navigateToSetting +import com.sopt.clody.presentation.ui.setting.navigation.navigateToWebView +import com.sopt.clody.presentation.ui.setting.navigation.notificationSettingScreen +import com.sopt.clody.presentation.ui.setting.navigation.settingScreen +import com.sopt.clody.presentation.ui.setting.navigation.webViewScreen +import com.sopt.clody.presentation.ui.splash.navigation.splashScreen +import com.sopt.clody.presentation.ui.writediary.navigation.navigateToWriteDiary +import com.sopt.clody.presentation.ui.writediary.navigation.writeDiaryScreen +import com.sopt.clody.presentation.utils.navigation.safePopBackStack + +/** + * 앱 전역의 Navigation Host. + * + * 각 기능? 화면?의 navigation graph를 이곳에 연결. + * [NavHost]를 사용하여 화면 전환을 구성하고, + * startIntent 등을 통해 초기 경로 조건 처리도 할 수 있음. + * + * @param appState 앱 상태를 포함한 네비게이션 컨트롤러 + * @param modifier Compose UI Modifier + * @param startIntent 외부에서 전달된 인텐트 (푸시 처리 등) + */ +@Composable +fun ClodyNavHost( + appState: ClodyAppState, + modifier: Modifier = Modifier, + startIntent: Intent, +) { + val navController: NavHostController = appState.navController + + Box(modifier = modifier.fillMaxSize()) { + NavHost( + navController = navController, + startDestination = appState.startDestination, + ) { + splashScreen( + startIntent = startIntent, + navigateToLogin = navController::navigateToLogin, + navigateToHome = navController::navigateToHome, + ) + + loginScreen( + navigateToTerms = navController::navigateToTermsOfService, + navigateToHome = navController::navigateToHome, + ) + termsOfServiceScreen( + navigateToNickname = navController::navigateToNickname, + navigateToLogin = navController::navigateToLogin, + ) + nicknameScreen( + navigateToReminder = navController::navigateToTimeReminder, + navigateToPrevious = navController::safePopBackStack, + ) + timeReminderScreen( + navigateToGuide = navController::navigateToGuide, + ) + guideScreen( + navigateToHome = navController::navigateToHome, + ) + homeScreen( + navigateToDiaryList = navController::navigateToDiaryList, + navigateToSetting = navController::navigateToSetting, + navigateToWriteDiary = navController::navigateToWriteDiary, + navigateToReplyLoading = navController::navigateToReplyLoading, + ) + diaryListScreen( + navigateToHome = navController::navigateToHome, + navigateToReplyLoading = navController::navigateToReplyLoading, + ) + writeDiaryScreen( + navigateToReplyLoading = navController::navigateToReplyLoading, + navigateToHome = navController::navigateToHome, + navigateToPrevious = navController::safePopBackStack, + ) + replyLoadingScreen( + navigateToReplyDiary = navController::navigateToReplyDiary, + navigateToHome = navController::navigateToHome, + navigateToDiaryList = navController::navigateToDiaryList, + ) + replyDiaryScreen( + navigateToHome = navController::navigateToHome, + ) + settingScreen( + navigateToAccountManagement = navController::navigateToAccountManagement, + navigateToNotification = navController::navigateToNotificationSetting, + navigateToPrevious = navController::safePopBackStack, + navigateToWebView = navController::navigateToWebView, + ) + accountManagementScreen( + navigateToPrevious = navController::safePopBackStack, + navigateToLogin = navController::navigateToLogin, + ) + notificationSettingScreen( + navigateToPrevious = navController::safePopBackStack, + ) + webViewScreen( + navigateToPrevious = navController::safePopBackStack, + ) + } + } +} From 5618202ddd464dba3952d0bb1fad87e8bcab3d64 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:08:29 +0900 Subject: [PATCH 005/299] =?UTF-8?q?[ADD/#260]=20ClodyAppState=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EB=B0=8F=20rememberClodyAppState=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80(=EC=95=B1=EC=9D=98=20?= =?UTF-8?q?=EC=A0=84=EC=97=AD=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/main/ClodyAppState.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyAppState.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyAppState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyAppState.kt new file mode 100644 index 00000000..ecbb4abf --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyAppState.kt @@ -0,0 +1,29 @@ +package com.sopt.clody.presentation.ui.main + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.sopt.clody.presentation.utils.navigation.Route + +/** + * 앱의 전역 상태를 관리하는 클래스. + * 현재는 네비게이션 컨트롤러만 포함되어 있으며, + * 추후 다른 상태(예: 사용자 인증 상태, 테마 설정 등) 추가 가능. + */ +@Stable +class ClodyAppState( + val navController: NavHostController, +) { + val startDestination = Route.Splash +} + +/** + * [ClodyAppState]를 기억하고 유지하는 Composable 함수. + * [rememberNavController]를 활용해 NavController를 생성. + */ +@Composable +fun rememberClodyAppState( + navController: NavHostController = rememberNavController(), +): ClodyAppState = remember { ClodyAppState(navController) } From 38808ca8062b63c3f782d46add3016e341381f9c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:08:38 +0900 Subject: [PATCH 006/299] =?UTF-8?q?[ADD/#260]=20ClodyApp=20=EB=A3=A8?= =?UTF-8?q?=ED=8A=B8=20Composable=20=ED=95=A8=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80(=EC=95=B1=20=ED=85=8C=EB=A7=88=20=EB=B0=8F=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=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 --- .../clody/presentation/ui/main/ClodyApp.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyApp.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyApp.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyApp.kt new file mode 100644 index 00000000..8907a986 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyApp.kt @@ -0,0 +1,39 @@ +package com.sopt.clody.presentation.ui.main + +import android.content.Intent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints +import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils + +/** + * 루트 Composable 함수. + * 앱 전반의 테마와 시스템 UI 설정, Scaffold 레이아웃을 관리하고 + * [ClodyNavHost]를 통해 실제 네비게이션 경로를 구성. + */ +@Composable +fun ClodyApp( + appState: ClodyAppState, + startIntent: Intent, +) { + LaunchedEffect(startIntent) { + if (startIntent.hasExtra("google.message_id")) { + AmplitudeUtils.trackEvent(AmplitudeConstraints.ALARM) + } + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + content = { innerPadding -> + ClodyNavHost( + appState = appState, + modifier = Modifier.padding(innerPadding), + startIntent = startIntent, + ) + }, + ) +} From 1a85489845c484942095f9e98153eb29b4138e2a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:08:55 +0900 Subject: [PATCH 007/299] =?UTF-8?q?[REFACTOR/#260]=20MainActivity=EC=97=90?= =?UTF-8?q?=EC=84=9C=20ClodyApp=EC=9C=BC=EB=A1=9C=20=EC=A0=84=ED=99=98?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=95=B1=20=EC=83=81=ED=83=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EB=82=B4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/main/MainActivity.kt | 77 +------------------ 1 file changed, 4 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/MainActivity.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/MainActivity.kt index bc1e827f..adcf751f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/main/MainActivity.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/MainActivity.kt @@ -1,31 +1,12 @@ package com.sopt.clody.presentation.ui.main import android.annotation.SuppressLint -import android.content.Intent import android.content.pm.ActivityInfo import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.navigation.compose.rememberNavController -import com.sopt.clody.presentation.ui.auth.navigation.AuthNavigator -import com.sopt.clody.presentation.ui.diarylist.navigation.DiaryListNavigator -import com.sopt.clody.presentation.ui.home.navigation.HomeNavigator -import com.sopt.clody.presentation.ui.navigatior.MainNavHost -import com.sopt.clody.presentation.ui.replydiary.navigation.ReplyDiaryNavigator -import com.sopt.clody.presentation.ui.replyloading.navigation.ReplyLoadingNavigator -import com.sopt.clody.presentation.ui.setting.navigation.SettingNavigator -import com.sopt.clody.presentation.ui.writediary.navigation.WriteDiaryNavigator -import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints -import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.ui.theme.CLODYTheme import dagger.hilt.android.AndroidEntryPoint @@ -36,6 +17,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + enableEdgeToEdge( statusBarStyle = SystemBarStyle.light( android.graphics.Color.TRANSPARENT, @@ -46,63 +28,12 @@ class MainActivity : ComponentActivity() { android.graphics.Color.WHITE, ), ) + setContent { CLODYTheme { - val navController = rememberNavController() - val currentIntent by rememberUpdatedState(newValue = intent) - var processedIntent by remember { mutableStateOf(null) } - - LaunchedEffect(key1 = currentIntent) { - if (processedIntent != currentIntent) { - handleIntent(currentIntent) - processedIntent = currentIntent - } - } - - SideEffect { - if (processedIntent != intent) { - handleIntent(intent) - processedIntent = intent - } - } - - LaunchedEffect(key1 = intent.getBooleanExtra("NAVIGATE_TO_LOGIN", false)) { - if (intent.getBooleanExtra("NAVIGATE_TO_LOGIN", false)) { - navController.navigate("register_graph") { - popUpTo(0) { inclusive = true } - } - } - } - - val authNavigator = remember(navController) { AuthNavigator(navController) } - val homeNavigator = remember(navController) { HomeNavigator(navController) } - val diaryListNavigator = remember(navController) { DiaryListNavigator(navController) } - val writeDiaryNavigator = remember(navController) { WriteDiaryNavigator(navController) } - val settingNavigator = remember(navController) { SettingNavigator(navController) } - val replyLoadingNavigator = remember(navController) { ReplyLoadingNavigator(navController) } - val replyDiaryNavigator = remember(navController) { ReplyDiaryNavigator(navController) } - - MainNavHost( - navController = navController, - authNavigator = authNavigator, - homeNavigator = homeNavigator, - diaryListNavigator = diaryListNavigator, - writeDiaryNavigator = writeDiaryNavigator, - settingNavigator = settingNavigator, - replyLoadingNavigator = replyLoadingNavigator, - replyDiaryNavigator = replyDiaryNavigator, - ) + val appState = rememberClodyAppState() + ClodyApp(appState = appState, startIntent = intent) } } } - - private fun handleIntent(intent: Intent?) { - if (intent?.extras?.containsKey("google.message_id") == true) { - logPushClickEvent() - } - } - - private fun logPushClickEvent() { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.ALARM) - } } From 2c7732cdd924eec766ddc991b7451520ba1b2bf8 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:09:23 +0900 Subject: [PATCH 008/299] =?UTF-8?q?[DEL/#260]=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=ED=98=B8=EC=8A=A4=ED=8A=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/navigatior/MainNavHost.kt | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/navigatior/MainNavHost.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/navigatior/MainNavHost.kt b/app/src/main/java/com/sopt/clody/presentation/ui/navigatior/MainNavHost.kt deleted file mode 100644 index 385666f8..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/navigatior/MainNavHost.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.sopt.clody.presentation.ui.navigatior - -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import com.sopt.clody.presentation.ui.auth.navigation.AuthNavigator -import com.sopt.clody.presentation.ui.auth.navigation.guidNavGraph -import com.sopt.clody.presentation.ui.auth.navigation.nicknameNavGraph -import com.sopt.clody.presentation.ui.auth.navigation.registerNavGraph -import com.sopt.clody.presentation.ui.auth.navigation.termsOfServiceNavGraph -import com.sopt.clody.presentation.ui.auth.navigation.timeReminderNavGraph -import com.sopt.clody.presentation.ui.diarylist.navigation.DiaryListNavigator -import com.sopt.clody.presentation.ui.diarylist.navigation.diaryListNavGraph -import com.sopt.clody.presentation.ui.home.navigation.HomeNavigator -import com.sopt.clody.presentation.ui.home.navigation.homeNavGraph -import com.sopt.clody.presentation.ui.replydiary.navigation.ReplyDiaryNavigator -import com.sopt.clody.presentation.ui.replyloading.navigation.ReplyLoadingNavigator -import com.sopt.clody.presentation.ui.replyloading.navigation.replyLoadingNavGraph -import com.sopt.clody.presentation.ui.setting.navigation.SettingNavigator -import com.sopt.clody.presentation.ui.setting.navigation.accountManagementNavGraph -import com.sopt.clody.presentation.ui.setting.navigation.notificationSettingNavGraph -import com.sopt.clody.presentation.ui.setting.navigation.settingNavGraph -import com.sopt.clody.presentation.ui.setting.navigation.webViewNavGraph -import com.sopt.clody.presentation.ui.splash.SplashRoute -import com.sopt.clody.presentation.ui.writediary.navigation.WriteDiaryNavigator -import com.sopt.clody.presentation.ui.writediary.navigation.writeDiaryNavGraph - -@Composable -fun MainNavHost( - navController: NavHostController, - modifier: Modifier = Modifier, - authNavigator: AuthNavigator, - homeNavigator: HomeNavigator, - diaryListNavigator: DiaryListNavigator, - writeDiaryNavigator: WriteDiaryNavigator, - settingNavigator: SettingNavigator, - replyLoadingNavigator: ReplyLoadingNavigator, - replyDiaryNavigator: ReplyDiaryNavigator, -) { - Box( - modifier = modifier, - ) { - NavHost( - navController = navController, - startDestination = "splash", - ) { - composable("splash") { SplashRoute(navigator = authNavigator) } - registerNavGraph(authNavigator) - termsOfServiceNavGraph(authNavigator) - nicknameNavGraph(authNavigator) - guidNavGraph(authNavigator) - timeReminderNavGraph(authNavigator) - homeNavGraph(homeNavigator) - diaryListNavGraph(diaryListNavigator) - writeDiaryNavGraph(writeDiaryNavigator) - settingNavGraph(settingNavigator) - accountManagementNavGraph(settingNavigator) - notificationSettingNavGraph(settingNavigator) - webViewNavGraph(settingNavigator) - replyLoadingNavGraph(replyLoadingNavigator, replyDiaryNavigator) - } - } -} From ccda88c2e8d7283dc6d86a6b8188a70ff751a411 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:10:09 +0900 Subject: [PATCH 009/299] =?UTF-8?q?[ADD/#260]=20=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EB=8B=B5=EC=9E=A5=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20enum=20class=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/utils/navigation/ReplyStatus.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/navigation/ReplyStatus.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/ReplyStatus.kt b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/ReplyStatus.kt new file mode 100644 index 00000000..d50e0e9c --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/ReplyStatus.kt @@ -0,0 +1,8 @@ +package com.sopt.clody.presentation.utils.navigation + +import kotlinx.serialization.Serializable + +@Serializable +enum class ReplyStatus { + UNREADY, READY_READ, READY_NOT_READ +} From de54c4934a9c089d3354245732119bb9567b0beb Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:11:15 +0900 Subject: [PATCH 010/299] =?UTF-8?q?[ADD/#260]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B5=AC=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/login/navigation/LoginNavigation.kt | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt new file mode 100644 index 00000000..9d728d7a --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt @@ -0,0 +1,26 @@ +package com.sopt.clody.presentation.ui.auth.login.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.composable +import com.sopt.clody.presentation.ui.auth.login.LoginRoute +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.loginScreen( + navigateToTerms: () -> Unit, + navigateToHome: () -> Unit, +) { + composable { + LoginRoute( + navigateToTerms = navigateToTerms, + navigateToHome = navigateToHome, + ) + } +} + +fun NavController.navigateToLogin( + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.Login, navOptions) +} From ea13105b2a8063cb9f61968f84b7a11bc3fa2485 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:12:18 +0900 Subject: [PATCH 011/299] =?UTF-8?q?[REFACTOR/#260]=20SignUpScreen=20->=20L?= =?UTF-8?q?oginScreen=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F?= =?UTF-8?q?=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SignUpScreen.kt => login/LoginScreen.kt} | 86 +++++++++---------- .../ui/auth/{signup => login}/SignInState.kt | 2 +- 2 files changed, 41 insertions(+), 47 deletions(-) rename app/src/main/java/com/sopt/clody/presentation/ui/auth/{signup/SignUpScreen.kt => login/LoginScreen.kt} (60%) rename app/src/main/java/com/sopt/clody/presentation/ui/auth/{signup => login}/SignInState.kt (71%) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt similarity index 60% rename from app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt index 8736d01f..96542b5f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.auth.signup +package com.sopt.clody.presentation.ui.auth.login import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -25,42 +25,37 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.sopt.clody.R import com.sopt.clody.presentation.ui.auth.component.button.KaKaoButton -import com.sopt.clody.presentation.ui.auth.navigation.AuthNavigator +import com.sopt.clody.presentation.ui.auth.signup.SignUpViewModel import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.utils.base.UiState import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage import com.sopt.clody.ui.theme.ClodyTheme @Composable -fun SignUpRoute( - authNavigator: AuthNavigator, +fun LoginRoute( + navigateToTerms: () -> Unit, + navigateToHome: () -> Unit, ) { val viewModel: SignUpViewModel = hiltViewModel() val signInState by viewModel.signInState.collectAsState() val context = LocalContext.current - LaunchedEffect(signInState) { + LaunchedEffect(signInState.uiState) { when (signInState.uiState) { - is UiState.Success -> { - authNavigator.navigateHome() - } - - is UiState.Failure -> { - authNavigator.navigateTermsOfService() - } - - else -> {} + is UiState.Success -> navigateToHome() + is UiState.Failure -> navigateToTerms() + else -> Unit } } - SignUpScreen( + LoginScreen( isLoading = signInState.uiState is UiState.Loading, onSignInClick = { viewModel.signInWithKakao(context) }, ) } @Composable -fun SignUpScreen( +fun LoginScreen( isLoading: Boolean, onSignInClick: () -> Unit, ) { @@ -85,34 +80,33 @@ fun SignUpScreen( .padding(bottom = 40.dp), ) }, - content = { innerPadding -> - Column( - modifier = Modifier - .fillMaxSize() - .background(color = backgroundColor) - .padding(innerPadding), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(modifier = Modifier.heightForScreenPercentage(0.38f)) - Image( - painter = painterResource(id = R.drawable.ic_signup_logo), - contentDescription = null, - contentScale = ContentScale.Crop, - ) - Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) - Image( - painter = painterResource(id = R.drawable.ic__signup_title), - contentDescription = null, - ) - Spacer(modifier = Modifier.heightForScreenPercentage(0.01f)) - Image( - painter = painterResource(id = R.drawable.ic_signup_logotitle), - contentDescription = null, - contentScale = ContentScale.Crop, - ) - } - }, - ) + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(color = backgroundColor) + .padding(innerPadding), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.heightForScreenPercentage(0.38f)) + Image( + painter = painterResource(id = R.drawable.ic_signup_logo), + contentDescription = null, + contentScale = ContentScale.Crop, + ) + Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) + Image( + painter = painterResource(id = R.drawable.ic__signup_title), + contentDescription = null, + ) + Spacer(modifier = Modifier.heightForScreenPercentage(0.01f)) + Image( + painter = painterResource(id = R.drawable.ic_signup_logotitle), + contentDescription = null, + contentScale = ContentScale.Crop, + ) + } + } if (isLoading) { LoadingScreen() @@ -121,8 +115,8 @@ fun SignUpScreen( @Preview(showBackground = true) @Composable -fun RegisterScreenPreview() { - SignUpScreen( +fun LoginScreenPreview() { + LoginScreen( isLoading = false, onSignInClick = {}, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignInState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/SignInState.kt similarity index 71% rename from app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignInState.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/auth/login/SignInState.kt index f3201b61..17fe8cce 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignInState.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/SignInState.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.auth.signup +package com.sopt.clody.presentation.ui.auth.login import com.sopt.clody.presentation.utils.base.UiState From 7057285e708b595042f1cac50ba220aff3518651 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:13:02 +0900 Subject: [PATCH 012/299] =?UTF-8?q?[ADD/#260]=20SignUpNavigation=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1(=EC=95=BD=EA=B4=80=20=EB=B0=8F=20=EB=8B=89?= =?UTF-8?q?=EB=84=A4=EC=9E=84=20=ED=99=94=EB=A9=B4=20=EB=82=B4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../signup/navigation/SignUpNavigation.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt new file mode 100644 index 00000000..da91b80e --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt @@ -0,0 +1,46 @@ +package com.sopt.clody.presentation.ui.auth.signup.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.composable +import com.sopt.clody.presentation.ui.auth.signup.NicknameRoute +import com.sopt.clody.presentation.ui.auth.signup.TermsOfServiceRoute +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.termsOfServiceScreen( + navigateToNickname: () -> Unit, + navigateToLogin: () -> Unit, +) { + composable { + TermsOfServiceRoute( + navigateToNickname = navigateToNickname, + navigateToLogin = navigateToLogin, + ) + } +} + +fun NavGraphBuilder.nicknameScreen( + navigateToReminder: () -> Unit, + navigateToPrevious: () -> Unit, +) { + composable { + NicknameRoute( + navigateToReminder = navigateToReminder, + navigateToPrevious = navigateToPrevious, + ) + } +} + +fun NavHostController.navigateToNickname( + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.Nickname, navOptions) +} + +fun NavController.navigateToTermsOfService( + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.TermsOfService, navOptions) +} From ac64caccb742859a935cfdb7683d99849986ac23 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:14:38 +0900 Subject: [PATCH 013/299] =?UTF-8?q?[REFACTOR/#260]=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B4=80=EB=A0=A8=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nickname 및 TermsOfService 화면 내 네비게이션 함수명을 새로운 구조에 맞게 수정 --- .../ui/auth/{screen => signup}/NicknameScreen.kt | 11 +++++------ .../presentation/ui/auth/signup/SignUpViewModel.kt | 1 + .../{screen => signup}/TermsOfServiceScreen.kt | 14 +++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) rename app/src/main/java/com/sopt/clody/presentation/ui/auth/{screen => signup}/NicknameScreen.kt (96%) rename app/src/main/java/com/sopt/clody/presentation/ui/auth/{screen => signup}/TermsOfServiceScreen.kt (95%) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/screen/NicknameScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameScreen.kt similarity index 96% rename from app/src/main/java/com/sopt/clody/presentation/ui/auth/screen/NicknameScreen.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameScreen.kt index b5626c91..dc17277e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/screen/NicknameScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameScreen.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.auth.screen +package com.sopt.clody.presentation.ui.auth.signup import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -41,8 +41,6 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R import com.sopt.clody.presentation.ui.auth.component.textfield.NickNameTextField -import com.sopt.clody.presentation.ui.auth.navigation.AuthNavigator -import com.sopt.clody.presentation.ui.auth.signup.SignUpViewModel import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.dialog.FailureDialog @@ -52,7 +50,8 @@ import com.sopt.clody.ui.theme.ClodyTheme @Composable fun NicknameRoute( - navigator: AuthNavigator, + navigateToReminder: () -> Unit, + navigateToPrevious: () -> Unit, viewModel: SignUpViewModel = hiltViewModel(), ) { val nickname by viewModel.nickname.collectAsState() @@ -67,7 +66,7 @@ fun NicknameRoute( nickname = nickname, onNicknameChange = viewModel::setNickname, onCompleteClick = { viewModel.proceedWithSignUp(context) }, - onBackClick = { navigator.navigateBack() }, + onBackClick = navigateToPrevious, isLoading = signUpState.uiState is UiState.Loading, isValidNickname = isValidNickname, nicknameMessage = nicknameMessage, @@ -76,7 +75,7 @@ fun NicknameRoute( LaunchedEffect(signUpState) { when (val result = signUpState.uiState) { is UiState.Success -> { - navigator.navigateTimeReminder() + navigateToReminder() } is UiState.Failure -> { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index a8b29806..11d3cfe6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -11,6 +11,7 @@ import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository +import com.sopt.clody.presentation.ui.auth.login.SignInState import com.sopt.clody.presentation.utils.base.UiState import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/screen/TermsOfServiceScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/TermsOfServiceScreen.kt similarity index 95% rename from app/src/main/java/com/sopt/clody/presentation/ui/auth/screen/TermsOfServiceScreen.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/TermsOfServiceScreen.kt index 7c1dd556..d9a5fffa 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/screen/TermsOfServiceScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/TermsOfServiceScreen.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.auth.screen +package com.sopt.clody.presentation.ui.auth.signup import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image @@ -30,7 +30,6 @@ import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.presentation.ui.auth.component.button.NextButton import com.sopt.clody.presentation.ui.auth.component.checkbox.CustomCheckbox -import com.sopt.clody.presentation.ui.auth.navigation.AuthNavigator import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls @@ -41,27 +40,28 @@ import kotlinx.coroutines.delay @Composable fun TermsOfServiceRoute( - navigator: AuthNavigator, + navigateToNickname: () -> Unit, + navigateToLogin: () -> Unit, ) { var backPressCount by remember { mutableStateOf(0) } LaunchedEffect(backPressCount) { if (backPressCount > 0) { - delay(2000) // 2 seconds delay + delay(2000) backPressCount = 0 } } BackHandler { if (backPressCount == 1) { - navigator.navigateToSignupScreen() + navigateToLogin() } else { backPressCount++ } } TermsOfServiceScreen( - onAgreeClick = { navigator.navigateNickname() }, - onBackClick = { navigator.navigateToSignupScreen() }, + onAgreeClick = navigateToNickname, + onBackClick = navigateToLogin, ) } From 2007896764d243a79fc218f09e2530160cddddc4 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:15:49 +0900 Subject: [PATCH 014/299] =?UTF-8?q?[ADD/#260]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timereminder/TimeReminderNavigation.kt | 23 +++++++++++++++++++ .../auth/timereminder/TimeReminderScreen.kt | 9 ++------ 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderNavigation.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderNavigation.kt new file mode 100644 index 00000000..ac9bf056 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderNavigation.kt @@ -0,0 +1,23 @@ +package com.sopt.clody.presentation.ui.auth.timereminder + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.composable +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.timeReminderScreen( + navigateToGuide: () -> Unit, +) { + composable { + TimeReminderRoute( + navigateToGuide = navigateToGuide, + ) + } +} + +fun NavHostController.navigateToTimeReminder( + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.TimeReminder, navOptions) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt index 67b254da..44726859 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt @@ -37,7 +37,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R import com.sopt.clody.presentation.ui.auth.component.container.PickerBox import com.sopt.clody.presentation.ui.auth.component.timepicker.BottomSheetTimePicker -import com.sopt.clody.presentation.ui.auth.navigation.AuthNavigator import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.dialog.FailureDialog @@ -49,7 +48,7 @@ import com.sopt.clody.ui.theme.ClodyTheme @Composable fun TimeReminderRoute( - navigator: AuthNavigator, + navigateToGuide: () -> Unit, viewModel: TimeReminderViewModel = hiltViewModel(), ) { val timeReminderState by viewModel.timeReminderState.collectAsState() @@ -81,15 +80,11 @@ fun TimeReminderRoute( // 알림 권한 요청 결과에 따른 처리 LaunchedEffect(timeReminderState) { when (val result = timeReminderState) { - is TimeReminderState.Success -> { - navigator.navigateGuide() - } - + is TimeReminderState.Success -> navigateToGuide() is TimeReminderState.Failure -> { showDialog = true dialogMessage = result.error } - else -> {} } } From 979260171765f74e2bc030a553e6f027c31983ae Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:16:10 +0900 Subject: [PATCH 015/299] =?UTF-8?q?[ADD/#260]=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=ED=99=94=EB=A9=B4=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B5=AC=EC=84=B1=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B2=BD=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 --- .../ui/auth/{screen => guide}/GuideScreen.kt | 7 +++--- .../auth/guide/navigation/GuideNavigation.kt | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) rename app/src/main/java/com/sopt/clody/presentation/ui/auth/{screen => guide}/GuideScreen.kt (97%) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/navigation/GuideNavigation.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/screen/GuideScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt similarity index 97% rename from app/src/main/java/com/sopt/clody/presentation/ui/auth/screen/GuideScreen.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt index 7a21c233..6611a7b8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/screen/GuideScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.auth.screen +package com.sopt.clody.presentation.ui.auth.guide import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween @@ -38,7 +38,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sopt.clody.R -import com.sopt.clody.presentation.ui.auth.navigation.AuthNavigator import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage import com.sopt.clody.ui.theme.ClodyTheme @@ -47,9 +46,9 @@ import kotlinx.coroutines.launch @Composable fun GuideRoute( - navigator: AuthNavigator, + navigateToHome: () -> Unit, ) { - GuideScreen(onNextButtonClick = { navigator.navigateHome() }) + GuideScreen(onNextButtonClick = navigateToHome) } @OptIn(ExperimentalFoundationApi::class) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/navigation/GuideNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/navigation/GuideNavigation.kt new file mode 100644 index 00000000..65f9590e --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/navigation/GuideNavigation.kt @@ -0,0 +1,24 @@ +package com.sopt.clody.presentation.ui.auth.guide.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.composable +import com.sopt.clody.presentation.ui.auth.guide.GuideRoute +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.guideScreen( + navigateToHome: () -> Unit, +) { + composable { + GuideRoute( + navigateToHome = navigateToHome, + ) + } +} + +fun NavController.navigateToGuide( + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.Guide, navOptions) +} From ed1d67272fe98e80d76948480b5898f308a1a792 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:16:30 +0900 Subject: [PATCH 016/299] =?UTF-8?q?[DEL/#260]=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=A0=9C=EA=B1=B0(Auth=EB=B6=80=EB=B6=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/navigation/AuthNavGraph.kt | 52 ------------------- .../ui/auth/navigation/AuthNavigator.kt | 36 ------------- 2 files changed, 88 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/navigation/AuthNavGraph.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/navigation/AuthNavigator.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/navigation/AuthNavGraph.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/navigation/AuthNavGraph.kt deleted file mode 100644 index 563ec440..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/navigation/AuthNavGraph.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.sopt.clody.presentation.ui.auth.navigation - -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import androidx.navigation.navigation -import com.sopt.clody.presentation.ui.auth.screen.GuideRoute -import com.sopt.clody.presentation.ui.auth.screen.NicknameRoute -import com.sopt.clody.presentation.ui.auth.screen.TermsOfServiceRoute -import com.sopt.clody.presentation.ui.auth.signup.SignUpRoute -import com.sopt.clody.presentation.ui.auth.timereminder.TimeReminderRoute - -fun NavGraphBuilder.registerNavGraph( - navigator: AuthNavigator, -) { - navigation(startDestination = "register", route = "register_graph") { - composable("register") { - SignUpRoute(navigator) - } - } -} - -fun NavGraphBuilder.termsOfServiceNavGraph( - navigator: AuthNavigator, -) { - composable("terms_of_service") { - TermsOfServiceRoute(navigator) - } -} - -fun NavGraphBuilder.nicknameNavGraph( - navigator: AuthNavigator, -) { - composable("nickname") { - NicknameRoute(navigator) - } -} - -fun NavGraphBuilder.guidNavGraph( - navigator: AuthNavigator, -) { - composable("guide") { - GuideRoute(navigator) - } -} - -fun NavGraphBuilder.timeReminderNavGraph( - navigator: AuthNavigator, -) { - composable("time_reminder") { - TimeReminderRoute(navigator) - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/navigation/AuthNavigator.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/navigation/AuthNavigator.kt deleted file mode 100644 index 693ceaaa..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/navigation/AuthNavigator.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.sopt.clody.presentation.ui.auth.navigation - -import androidx.navigation.NavHostController -import java.time.LocalDate - -class AuthNavigator( - val navController: NavHostController, -) { - val startDestination = "register_graph" - fun navigateTermsOfService() { - navController.navigate("terms_of_service") - } - fun navigateNickname() { - navController.navigate("nickname") - } - fun navigateGuide() { - navController.navigate("guide") - } - - fun navigateTimeReminder() { - navController.navigate("time_reminder") - } - - fun navigateHome(selectedYear: Int = LocalDate.now().year, selectedMonth: Int = LocalDate.now().monthValue) { - navController.navigate("home/$selectedYear/$selectedMonth") - } - - fun navigateBack() { - navController.navigateUp() - } - fun navigateToSignupScreen() { - navController.navigate("register") { - popUpTo(navController.graph.startDestinationId) { inclusive = true } - } - } -} From 3fd2b8c6070c6b8d728ed61e7e954951ae1a03d0 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:18:15 +0900 Subject: [PATCH 017/299] =?UTF-8?q?[ADD/#260]=20=EC=8A=A4=ED=94=8C?= =?UTF-8?q?=EB=9E=98=EC=8B=9C=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=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20SplashRoute=EC=97=90=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=EB=90=98=EB=8A=94=20=EC=9D=B8=EC=9E=90=EB=A5=BC=20?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=20=EA=B5=AC=EC=A1=B0=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/splash/SplashScreen.kt | 28 +++++++++++-------- .../ui/splash/navigation/SplashNavigation.kt | 21 ++++++++++++++ 2 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/splash/navigation/SplashNavigation.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt index 10505c0b..94fdc967 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt @@ -1,6 +1,7 @@ package com.sopt.clody.presentation.ui.splash import android.app.Activity +import android.content.Intent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -23,15 +24,17 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.clody.R import com.sopt.clody.domain.model.AppUpdateState -import com.sopt.clody.presentation.ui.auth.navigation.AuthNavigator +import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints +import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.appupdate.AppUpdateUtils import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.coroutines.delay -import java.time.LocalDate @Composable fun SplashRoute( - navigator: AuthNavigator, + startIntent: Intent, + onLoginRequired: () -> Unit, + onAlreadyLoggedIn: () -> Unit, viewModel: SplashViewModel = hiltViewModel(), ) { val isUserLoggedIn by viewModel.isUserLoggedIn.collectAsStateWithLifecycle() @@ -39,17 +42,20 @@ fun SplashRoute( val context = LocalContext.current val activity = context as Activity + // Push 클릭 추적 + LaunchedEffect(startIntent) { + if (startIntent.hasExtra("google.message_id")) { + AmplitudeUtils.trackEvent(AmplitudeConstraints.ALARM) + } + } + LaunchedEffect(isUserLoggedIn, updateState) { if (isUserLoggedIn != null && updateState == AppUpdateState.Latest) { delay(1000) - navigator.navController.navigate( - if (isUserLoggedIn == true) { - "home/${LocalDate.now().year}/${LocalDate.now().monthValue}" - } else { - "register_graph" - }, - ) { - popUpTo("splash") { inclusive = true } + if (isUserLoggedIn == true) { + onAlreadyLoggedIn() + } else { + onLoginRequired() } } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/navigation/SplashNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/navigation/SplashNavigation.kt new file mode 100644 index 00000000..e6ed33a2 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/navigation/SplashNavigation.kt @@ -0,0 +1,21 @@ +package com.sopt.clody.presentation.ui.splash.navigation + +import android.content.Intent +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.sopt.clody.presentation.ui.splash.SplashRoute +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.splashScreen( + startIntent: Intent, + navigateToLogin: () -> Unit, + navigateToHome: () -> Unit, +) { + composable { + SplashRoute( + startIntent = startIntent, + onLoginRequired = navigateToLogin, + onAlreadyLoggedIn = navigateToHome, + ) + } +} From 12f384e2146f623bfdffb4762a6d304be7fb3061 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:18:50 +0900 Subject: [PATCH 018/299] =?UTF-8?q?[ADD/#260]=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../setting/navigation/SettingNavigation.kt | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt new file mode 100644 index 00000000..e2228bc8 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt @@ -0,0 +1,86 @@ +package com.sopt.clody.presentation.ui.setting.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationSettingRoute +import com.sopt.clody.presentation.ui.setting.screen.AccountManagementRoute +import com.sopt.clody.presentation.ui.setting.screen.SettingRoute +import com.sopt.clody.presentation.ui.setting.screen.WebViewRoute +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.settingScreen( + navigateToAccountManagement: () -> Unit, + navigateToNotification: () -> Unit, + navigateToPrevious: () -> Unit, + navigateToWebView: (String) -> Unit, +) { + composable { + SettingRoute( + navigateToAccountManagement = navigateToAccountManagement, + navigateToNotification = navigateToNotification, + navigateToPrevious = navigateToPrevious, + navigateToWebView = navigateToWebView, + ) + } +} + +fun NavGraphBuilder.accountManagementScreen( + navigateToPrevious: () -> Unit, + navigateToLogin: () -> Unit, +) { + composable { + AccountManagementRoute( + navigateToPrevious = navigateToPrevious, + navigateToLogin = navigateToLogin, + ) + } +} + +fun NavGraphBuilder.notificationSettingScreen( + navigateToPrevious: () -> Unit, +) { + composable { + NotificationSettingRoute(navigateToPrevious = navigateToPrevious) + } +} + +fun NavGraphBuilder.webViewScreen( + navigateToPrevious: () -> Unit, +) { + composable { backStackEntry -> + backStackEntry.toRoute().apply { + WebViewRoute( + encodedUrl = encodedUrl, + navigateToPrevious = navigateToPrevious, + ) + } + } +} + +fun NavController.navigateToSetting( + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.Setting, navOptions) +} + +fun NavController.navigateToAccountManagement( + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.AccountManagement, navOptions) +} + +fun NavController.navigateToNotificationSetting( + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.NotificationSetting, navOptions) +} + +fun NavController.navigateToWebView( + encodedUrl: String, + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.WebView(encodedUrl), navOptions) +} From 9a83ecdb741e5018602006a87428bdc8b926f656 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:19:01 +0900 Subject: [PATCH 019/299] =?UTF-8?q?[DEL/#260]=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=EC=A1=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/setting/navigation/SettingNavGraph.kt | 48 ------------------- .../ui/setting/navigation/SettingNavigator.kt | 27 ----------- 2 files changed, 75 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavGraph.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigator.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavGraph.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavGraph.kt deleted file mode 100644 index 471df2f5..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavGraph.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.sopt.clody.presentation.ui.setting.navigation - -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationSettingRoute -import com.sopt.clody.presentation.ui.setting.screen.AccountManagementRoute -import com.sopt.clody.presentation.ui.setting.screen.SettingRoute -import com.sopt.clody.presentation.ui.setting.screen.WebViewRoute - -fun NavGraphBuilder.settingNavGraph( - navigator: SettingNavigator, -) { - composable("setting") { - SettingRoute(navigator) - } -} - -fun NavGraphBuilder.accountManagementNavGraph( - navigator: SettingNavigator, -) { - composable("account_management") { - AccountManagementRoute(navigator) - } -} - -fun NavGraphBuilder.notificationSettingNavGraph( - navigator: SettingNavigator, -) { - composable("notification_setting") { - NotificationSettingRoute(navigator) - } -} - -fun NavGraphBuilder.webViewNavGraph( - navigator: SettingNavigator, -) { - composable( - route = "web_view/{encodedUrl}", - arguments = listOf(navArgument("encodedUrl") { type = NavType.StringType }), - ) { backStackEntry -> - val encodedUrl = backStackEntry.arguments?.getString("encodedUrl") - encodedUrl?.let { - WebViewRoute(navigator, it) - } - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigator.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigator.kt deleted file mode 100644 index 4a317454..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigator.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.sopt.clody.presentation.ui.setting.navigation - -import androidx.navigation.NavHostController -import java.net.URLEncoder - -class SettingNavigator( - val navController: NavHostController, -) { - fun navigateAccountManagement() { - navController.navigate("account_management") - } - - fun navigateNotificationSetting() { - navController.navigate("notification_setting") - } - - fun navigateWebView(url: String) { - val encodedUrl = URLEncoder.encode(url, "UTF-8") - navController.navigate("web_view/$encodedUrl") - } - - fun navigateBack() { - if (navController.currentBackStackEntry?.lifecycle?.currentState == androidx.lifecycle.Lifecycle.State.RESUMED) { - navController.popBackStack() - } - } -} From bd3671260d89d20ab0344c64509fc613f8877f94 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:21:16 +0900 Subject: [PATCH 020/299] =?UTF-8?q?[REFACTOR/#260]=20=EC=84=B8=ED=8C=85?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=9D=98=20=EA=B0=81=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=EC=A0=84=EB=8B=AC=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=20=EC=88=98=EC=A0=95(=EB=84=A4=EB=B9=84=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/NotificationSettingScreen.kt | 5 ++--- .../ui/setting/screen/AccountManagementScreen.kt | 14 +++++--------- .../ui/setting/screen/SettingScreen.kt | 14 ++++++++------ .../ui/setting/screen/WebViewScreen.kt | 5 ++--- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index bf70651b..68848d9c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -29,7 +29,6 @@ import com.sopt.clody.presentation.ui.component.dialog.FailureDialog import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage import com.sopt.clody.presentation.ui.setting.component.SettingTopAppBar -import com.sopt.clody.presentation.ui.setting.navigation.SettingNavigator import com.sopt.clody.presentation.ui.setting.notificationsetting.component.DiaryAlarmSwitch import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSettingTime import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSettingTimePicker @@ -38,7 +37,7 @@ import com.sopt.clody.ui.theme.ClodyTheme @Composable fun NotificationSettingRoute( - navigator: SettingNavigator, + navigateToPrevious: () -> Unit, notificationSettingViewModel: NotificationSettingViewModel = hiltViewModel(), ) { val context = LocalContext.current @@ -75,7 +74,7 @@ fun NotificationSettingRoute( updateNotificationTimePicker = { state -> showNotificationTimePicker = state }, showFailureDialog = showFailureDialog, failureDialogMessage = failureDialogMessage, - onClickBack = { navigator.navigateBack() }, + onClickBack = navigateToPrevious, onNotificationInfoAvailable = { notificationInfo = it }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt index 5f4a6adf..a7dba39a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt @@ -30,14 +30,14 @@ import com.sopt.clody.presentation.ui.setting.component.LogoutDialog import com.sopt.clody.presentation.ui.setting.component.NicknameChangeBottomSheet import com.sopt.clody.presentation.ui.setting.component.SettingSeparateLine import com.sopt.clody.presentation.ui.setting.component.SettingTopAppBar -import com.sopt.clody.presentation.ui.setting.navigation.SettingNavigator import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.ui.theme.ClodyTheme @Composable fun AccountManagementRoute( - navigator: SettingNavigator, + navigateToPrevious: () -> Unit, + navigateToLogin: () -> Unit, accountManagementViewModel: AccountManagementViewModel = hiltViewModel(), ) { val userInfoState by accountManagementViewModel.userInfoState.collectAsState() @@ -64,17 +64,13 @@ fun AccountManagementRoute( LaunchedEffect(revokeAccountState) { if (revokeAccountState is RevokeAccountState.Success) { - navigator.navController.navigate("register_graph") { - popUpTo("home") { inclusive = true } - } + navigateToLogin() } } LaunchedEffect(logOutState) { if (logOutState is LogOutState.Success) { - navigator.navController.navigate("register_graph") { - popUpTo("home") { inclusive = true } - } + navigateToLogin() } } @@ -92,7 +88,7 @@ fun AccountManagementRoute( updateRevokeDialog = { state -> showRevokeDialog = state }, showFailureDialog = showFailureDialog, failureDialogMessage = failureDialogMessage, - onBackClick = { navigator.navigateBack() }, + onBackClick = navigateToPrevious, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt index fff76bd5..13edf99e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt @@ -21,14 +21,16 @@ import com.sopt.clody.presentation.ui.setting.component.SettingOption import com.sopt.clody.presentation.ui.setting.component.SettingSeparateLine import com.sopt.clody.presentation.ui.setting.component.SettingTopAppBar import com.sopt.clody.presentation.ui.setting.component.SettingVersionInfo -import com.sopt.clody.presentation.ui.setting.navigation.SettingNavigator import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.ui.theme.ClodyTheme @Composable fun SettingRoute( - navigator: SettingNavigator, + navigateToAccountManagement: () -> Unit, + navigateToNotification: () -> Unit, + navigateToPrevious: () -> Unit, + navigateToWebView: (String) -> Unit, settingViewModel: SettingViewModel = hiltViewModel(), ) { val versionInfo by settingViewModel::versionInfo @@ -40,10 +42,10 @@ fun SettingRoute( SettingScreen( versionInfo = versionInfo ?: stringResource(R.string.setting_version_info_failure), - onClickBack = { navigator.navigateBack() }, - onClickAccountManagement = { navigator.navigateAccountManagement() }, - onClickNotificationSetting = { navigator.navigateNotificationSetting() }, - onClickInquiriesSuggestions = { navigator.navigateWebView(SettingOptionUrls.INQUIRIES_SUGGESTIONS_URL) }, + onClickBack = navigateToPrevious, + onClickAccountManagement = navigateToAccountManagement, + onClickNotificationSetting = navigateToNotification, + onClickInquiriesSuggestions = { navigateToWebView(SettingOptionUrls.INQUIRIES_SUGGESTIONS_URL) }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/WebViewScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/WebViewScreen.kt index 7cfed543..c2e12e04 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/WebViewScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/WebViewScreen.kt @@ -16,16 +16,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView -import com.sopt.clody.presentation.ui.setting.navigation.SettingNavigator @Composable fun WebViewRoute( - navigator: SettingNavigator, + navigateToPrevious: () -> Unit, encodedUrl: String, ) { WebViewScreen( encodedUrl = encodedUrl, - onClickBack = { navigator.navigateBack() }, + onClickBack = navigateToPrevious, ) } From ec256f9fed8262d76ef5c0bbe7549d4b66eb19cb Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:21:57 +0900 Subject: [PATCH 021/299] =?UTF-8?q?[ADD/#260]=20=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../navigation/WriteDiaryNavigation.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavigation.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavigation.kt new file mode 100644 index 00000000..bdce0660 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavigation.kt @@ -0,0 +1,37 @@ +package com.sopt.clody.presentation.ui.writediary.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.sopt.clody.presentation.ui.writediary.screen.WriteDiaryRoute +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.writeDiaryScreen( + navigateToReplyLoading: (year: Int, month: Int, day: Int) -> Unit, + navigateToHome: (year: Int, month: Int) -> Unit, + navigateToPrevious: () -> Unit, +) { + composable { backStackEntry -> + backStackEntry.toRoute().apply { + WriteDiaryRoute( + year = year, + month = month, + date = date, + navigateToReplyLoading = navigateToReplyLoading, + navigateToHome = navigateToHome, + navigateToPrevious = navigateToPrevious, + ) + } + } +} + +fun NavController.navigateToWriteDiary( + year: Int, + month: Int, + day: Int, + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.WriteDiary(year, month, day), navOptions) +} From 85fd18b4e8c8151d78ad1659ea817b8e52aa27c2 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:22:12 +0900 Subject: [PATCH 022/299] =?UTF-8?q?[DEL/#260]=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=9D=BC=EA=B8=B0=EC=9E=91=EC=84=B1=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC=EC=A1=B0=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../navigation/WriteDiaryNavGraph.kt | 27 ------------------- .../navigation/WriteDiaryNavigator.kt | 17 ------------ 2 files changed, 44 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavGraph.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavigator.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavGraph.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavGraph.kt deleted file mode 100644 index 26703461..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavGraph.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.sopt.clody.presentation.ui.writediary.navigation - -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import com.sopt.clody.presentation.ui.writediary.screen.WriteDiaryRoute -import java.time.LocalDate - -fun NavGraphBuilder.writeDiaryNavGraph( - writeDiaryNavigator: WriteDiaryNavigator, -) { - composable( - "write_diary/{year}/{month}/{day}", - arguments = listOf( - navArgument("year") { type = NavType.IntType }, - navArgument("month") { type = NavType.IntType }, - navArgument("day") { type = NavType.IntType }, - ), - ) { backStackEntry -> - val currentDate = LocalDate.now() - val year = backStackEntry.arguments?.getInt("year") ?: currentDate.year - val month = backStackEntry.arguments?.getInt("month") ?: currentDate.monthValue - val day = backStackEntry.arguments?.getInt("day") ?: currentDate.dayOfMonth - WriteDiaryRoute(writeDiaryNavigator, year, month, day) - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavigator.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavigator.kt deleted file mode 100644 index 9450a6cc..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/navigation/WriteDiaryNavigator.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.sopt.clody.presentation.ui.writediary.navigation - -import androidx.navigation.NavHostController - -class WriteDiaryNavigator( - val navController: NavHostController, -) { - fun navigateReplyLoading(year: Int, month: Int, day: Int, replyStatus: String = "READY_NOT_READ") { - navController.navigate("reply_loading/$year/$month/$day?from=write_diary&replyStatus=$replyStatus") - } - fun navigateHome(selectedYear: Int, selectedMonth: Int) { - navController.navigate("home/$selectedYear/$selectedMonth") - } - fun navigateBack() { - navController.navigateUp() - } -} From 158c52189fa710829c5e81cd3eb09a1a6b2f13cc Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:23:07 +0900 Subject: [PATCH 023/299] =?UTF-8?q?[REFACTOR/#260]=20=EB=84=A4=EB=B9=84=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EC=9D=BC=EA=B8=B0=EC=9E=91=EC=84=B1=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8A=B8=20=EC=9D=B8=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/writediary/screen/WriteDiaryScreen.kt | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index 31db1810..a931a59e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -44,7 +44,6 @@ import com.sopt.clody.presentation.ui.writediary.component.bottomsheet.DeleteWri import com.sopt.clody.presentation.ui.writediary.component.text.DiaryTitleText import com.sopt.clody.presentation.ui.writediary.component.textfield.WriteDiaryTextField import com.sopt.clody.presentation.ui.writediary.component.tooltip.TooltipIcon -import com.sopt.clody.presentation.ui.writediary.navigation.WriteDiaryNavigator import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.getDayOfWeek @@ -53,10 +52,12 @@ import com.sopt.clody.ui.theme.ClodyTheme @Composable fun WriteDiaryRoute( - navigator: WriteDiaryNavigator, year: Int, month: Int, - day: Int, + date: Int, + navigateToReplyLoading: (year: Int, month: Int, date: Int) -> Unit, + navigateToHome: (year: Int, month: Int) -> Unit, + navigateToPrevious: () -> Unit, viewModel: WriteDiaryViewModel = hiltViewModel(), ) { val entries = viewModel.entries @@ -76,8 +77,8 @@ fun WriteDiaryRoute( LaunchedEffect(writeDiaryState) { when (writeDiaryState) { - is WriteDiaryState.Success -> navigator.navigateReplyLoading(year, month, day) - is WriteDiaryState.NoReply -> navigator.navigateHome(year, month) + is WriteDiaryState.Success -> navigateToReplyLoading(year, month, date) + is WriteDiaryState.NoReply -> navigateToHome(year, month) is WriteDiaryState.Failure -> viewModel.updateShowDialog(false) else -> {} } @@ -96,12 +97,12 @@ fun WriteDiaryRoute( showDialog = showDialog, onClickBack = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.WRITING_DIARY_BACK) - navigator.navigateHome(year, month) + navigateToPrevious() }, - onCompleteClick = { viewModel.writeDiary(year, month, day, entries) }, + onCompleteClick = { viewModel.writeDiary(year, month, date, entries) }, year = year, month = month, - day = day, + day = date, ) if (showFailureDialog) { From 7354894fa680528a827642586b72daa1a6e55fa3 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:23:35 +0900 Subject: [PATCH 024/299] =?UTF-8?q?[ADD/#260]=20=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=EB=A1=9C=EB=94=A9=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC=EC=A1=B0=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 --- .../navigation/ReplyLoadingNavigation.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt new file mode 100644 index 00000000..e793b3c0 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt @@ -0,0 +1,45 @@ +package com.sopt.clody.presentation.ui.replyloading.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.sopt.clody.presentation.ui.replyloading.screen.ReplyLoadingRoute +import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.replyLoadingScreen( + navigateToReplyDiary: (Int, Int, Int, ReplyStatus) -> Unit, + navigateToHome: (year: Int, month: Int, day: Int) -> Unit, + navigateToDiaryList: (Int, Int) -> Unit, +) { + composable { backStackEntry -> + backStackEntry.toRoute().apply { + ReplyLoadingRoute( + year = year, + month = month, + date = date, + from = from, + replyStatus = replyStatus, + navigateToReplyDiary = navigateToReplyDiary, + navigateToHome = navigateToHome, + navigateToDiaryList = navigateToDiaryList, + ) + } + } +} + +fun NavController.navigateToReplyLoading( + year: Int, + month: Int, + day: Int, + from: Route.ReplyLoading.ReplyLoadingFrom = Route.ReplyLoading.ReplyLoadingFrom.HOME, + replyStatus: ReplyStatus = ReplyStatus.UNREADY, + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate( + Route.ReplyLoading(year, month, day, from, replyStatus), + navOptions, + ) +} From e12a55627251acc627fcf933792b6bd3880215f1 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:23:44 +0900 Subject: [PATCH 025/299] =?UTF-8?q?[DEL/#260]=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=9D=BC=EA=B8=B0=EC=9D=91=EB=8B=B5=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../navigation/ReplyLoadingNavGraph.kt | 49 ------------------- .../navigation/ReplyLoadingNavigator.kt | 47 ------------------ 2 files changed, 96 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavGraph.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigator.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavGraph.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavGraph.kt deleted file mode 100644 index 243d1d6e..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavGraph.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.sopt.clody.presentation.ui.replyloading.navigation - -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import com.sopt.clody.presentation.ui.replydiary.ReplyDiaryRoute -import com.sopt.clody.presentation.ui.replydiary.navigation.ReplyDiaryNavigator -import com.sopt.clody.presentation.ui.replyloading.screen.ReplyLoadingRoute -import java.time.LocalDate - -fun NavGraphBuilder.replyLoadingNavGraph( - replyLoadingNavigator: ReplyLoadingNavigator, - replyDiaryNavigator: ReplyDiaryNavigator, -) { - val currentDate = LocalDate.now() - composable( - "reply_loading/{year}/{month}/{day}?from={from}&replyStatus={replyStatus}", - arguments = listOf( - navArgument("year") { type = NavType.IntType }, - navArgument("month") { type = NavType.IntType }, - navArgument("day") { type = NavType.IntType }, - navArgument("from") { defaultValue = "home" }, - navArgument("replyStatus") { defaultValue = "UNREADY" }, - ), - ) { backStackEntry -> - val year = backStackEntry.arguments?.getInt("year") ?: currentDate.year - val month = backStackEntry.arguments?.getInt("month") ?: currentDate.monthValue - val day = backStackEntry.arguments?.getInt("day") ?: currentDate.dayOfMonth - val from = backStackEntry.arguments?.getString("from") ?: "home" - val replyStatus = backStackEntry.arguments?.getString("replyStatus") ?: "UNREADY" - ReplyLoadingRoute(replyLoadingNavigator, year, month, day, from, replyStatus) - } - composable( - "reply_diary/{year}/{month}/{day}?replyStatus={replyStatus}", - arguments = listOf( - navArgument("year") { type = NavType.IntType }, - navArgument("month") { type = NavType.IntType }, - navArgument("day") { type = NavType.IntType }, - navArgument("replyStatus") { defaultValue = "UNREADY" }, - ), - ) { backStackEntry -> - val year = backStackEntry.arguments?.getInt("year") ?: currentDate.year - val month = backStackEntry.arguments?.getInt("month") ?: currentDate.monthValue - val day = backStackEntry.arguments?.getInt("day") ?: currentDate.dayOfMonth - val replyStatus = backStackEntry.arguments?.getString("replyStatus") ?: "UNREADY" - ReplyDiaryRoute(replyDiaryNavigator, year, month, day, replyStatus) - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigator.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigator.kt deleted file mode 100644 index 1653ca08..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigator.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.sopt.clody.presentation.ui.replyloading.navigation - -import androidx.navigation.NavHostController - -class ReplyLoadingNavigator( - private val navController: NavHostController, -) { - fun navigateHome(selectedYear: Int, selectedMonth: Int) { - navController.navigate("home/$selectedYear/$selectedMonth") { - popUpTo(navController.graph.startDestinationId) { - inclusive = true - } - } - } - - fun navigateReplyDiary(year: Int, month: Int, day: Int, replyStatus: String) { - navController.navigate("reply_diary/$year/$month/$day?replyStatus=$replyStatus") - } - - private fun navigateWithPopUp(route: String, inclusive: Boolean = false) { - navController.navigate(route) { - popUpTo(navController.graph.startDestinationId) { - this.inclusive = inclusive - } - } - } - - fun navigateBack(selectedYear: Int, selectedMonth: Int, from: String) { - when (from) { - "diary_list" -> { - navigateWithPopUp("diary_list/$selectedYear/$selectedMonth") - } - - "home" -> { - navigateWithPopUp("home/$selectedYear/$selectedMonth") - } - - "write_diary" -> { - navigateWithPopUp("home/$selectedYear/$selectedMonth") // 예외적으로 홈으로 - } - - else -> { - navController.navigateUp() - } - } - } -} From 6f045a4f8659253ff2f282dd4227cecb89872967 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:24:40 +0900 Subject: [PATCH 026/299] =?UTF-8?q?[ADD/#260]=20=EB=84=A4=EB=B9=84=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EC=9D=BC=EA=B8=B0=EC=9D=91=EB=8B=B5=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=ED=99=94=EB=A9=B4=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../replyloading/screen/ReplyLoadingScreen.kt | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt index 400dfb9c..f9d0e939 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt @@ -43,22 +43,25 @@ import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage import com.sopt.clody.presentation.ui.replyloading.component.LottieAnimation import com.sopt.clody.presentation.ui.replyloading.component.QuickReplyAdButton -import com.sopt.clody.presentation.ui.replyloading.navigation.ReplyLoadingNavigator import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage +import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.coroutines.delay import java.time.LocalDateTime @Composable fun ReplyLoadingRoute( - navigator: ReplyLoadingNavigator, year: Int, month: Int, - day: Int, - from: String, - replyStatus: String, + date: Int, + from: Route.ReplyLoading.ReplyLoadingFrom, + replyStatus: ReplyStatus, + navigateToReplyDiary: (Int, Int, Int, ReplyStatus) -> Unit, + navigateToHome: (Int, Int, Int) -> Unit, + navigateToDiaryList: (Int, Int) -> Unit, viewModel: ReplyLoadingViewModel = hiltViewModel(), ) { val replyLoadingState by viewModel.replyLoadingState.collectAsState() @@ -71,16 +74,24 @@ fun ReplyLoadingRoute( LaunchedEffect(Unit) { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.WAITING_DIARY) - viewModel.getDiaryTime(year, month, day) + viewModel.getDiaryTime(year, month, date) } var backPressedTime by remember { mutableStateOf(0L) } val backPressThreshold = 2000 + val handleBackNavigation = { + when (from) { + Route.ReplyLoading.ReplyLoadingFrom.HOME -> navigateToHome(year, month, date) + + Route.ReplyLoading.ReplyLoadingFrom.DIARY_LIST -> navigateToDiaryList(year, month) + } + } + BackHandler { val currentTime = System.currentTimeMillis() if (currentTime - backPressedTime <= backPressThreshold) { - navigator.navigateHome(year, month) + handleBackNavigation() } else { backPressedTime = currentTime } @@ -94,8 +105,10 @@ fun ReplyLoadingRoute( is ReplyLoadingState.Success -> { val successState = replyLoadingState as ReplyLoadingState.Success ReplyLoadingScreen( - onCompleteClick = { navigator.navigateReplyDiary(year, month, day, replyStatus) }, - onBackClick = { navigator.navigateBack(year, month, from) }, + onCompleteClick = { + navigateToReplyDiary(year, month, date, replyStatus) + }, + onBackClick = { handleBackNavigation() }, replyLoadingState = successState, onShowAdClick = { viewModel.loadAndShowRewardedAd(activity) From d30643b1674e117f8ef02a78843539090d7acfb0 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:25:52 +0900 Subject: [PATCH 027/299] =?UTF-8?q?[REFACTOR/#260]=20=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../navigation/ReplyDiaryNavigation.kt | 36 +++++++++++++++++++ .../navigation/ReplyDiaryNavigator.kt | 19 ---------- 2 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigator.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt new file mode 100644 index 00000000..e06469bb --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt @@ -0,0 +1,36 @@ +package com.sopt.clody.presentation.ui.replydiary.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.sopt.clody.presentation.ui.replydiary.ReplyDiaryRoute +import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.replyDiaryScreen( + navigateToHome: (year: Int, month: Int, date: Int) -> Unit, +) { + composable { backStackEntry -> + backStackEntry.toRoute().apply { + ReplyDiaryRoute( + year = year, + month = month, + date = date, + replyStatus = replyStatus, + navigateToHome = navigateToHome, + ) + } + } +} + +fun NavController.navigateToReplyDiary( + year: Int, + month: Int, + day: Int, + replyStatus: ReplyStatus = ReplyStatus.UNREADY, + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.ReplyDiary(year, month, day, replyStatus), navOptions) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigator.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigator.kt deleted file mode 100644 index 419dd1af..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigator.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.sopt.clody.presentation.ui.replydiary.navigation - -import androidx.navigation.NavHostController - -class ReplyDiaryNavigator( - val navController: NavHostController, -) { - fun navigateHome(selectedYear: Int, selectedMonth: Int) { - navController.navigate("home/$selectedYear/$selectedMonth") { - popUpTo(navController.graph.startDestinationId) { - inclusive = true - } - } - } - - fun navigateBack() { - navController.navigateUp() - } -} From 6fa98c99a897d1db8638386d09c043dcb77cb458 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:26:10 +0900 Subject: [PATCH 028/299] =?UTF-8?q?[REFACTOR/#260]=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=B8=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/replydiary/ReplyDiaryScreen.kt | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt index f68ddafb..b83c8a01 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt @@ -37,19 +37,19 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen -import com.sopt.clody.presentation.ui.replydiary.navigation.ReplyDiaryNavigator import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage +import com.sopt.clody.presentation.utils.navigation.ReplyStatus import com.sopt.clody.ui.theme.ClodyTheme @Composable fun ReplyDiaryRoute( - navigator: ReplyDiaryNavigator, year: Int, month: Int, date: Int, - replyStatus: String, + replyStatus: ReplyStatus, + navigateToHome: (year: Int, month: Int, date: Int) -> Unit, viewModel: ReplyDiaryViewModel = hiltViewModel(), ) { val replyDiaryState by viewModel.replyDiaryState.collectAsState() @@ -65,7 +65,7 @@ fun ReplyDiaryRoute( BackHandler { val currentTime = System.currentTimeMillis() if (currentTime - backPressedTime <= backPressThreshold) { - navigator.navigateHome(year, month) + navigateToHome(year, month, date) } else { backPressedTime = currentTime } @@ -79,7 +79,7 @@ fun ReplyDiaryRoute( is ReplyDiaryState.Success -> { val successState = replyDiaryState as ReplyDiaryState.Success ReplyDiaryScreen( - onClickBack = { navigator.navigateHome(year, month) }, + navigateToHome = { navigateToHome(year, month, date) }, replyStatus = replyStatus, replyDiaryState = successState, ) @@ -99,18 +99,17 @@ fun ReplyDiaryRoute( @OptIn(ExperimentalMaterial3Api::class) @Composable fun ReplyDiaryScreen( - onClickBack: () -> Unit, - replyStatus: String, + navigateToHome: () -> Unit, + replyStatus: ReplyStatus, replyDiaryState: ReplyDiaryState.Success, ) { var showDialog by remember { mutableStateOf(false) } LaunchedEffect(replyDiaryState) { - if (replyStatus == "READY_NOT_READ") { + if (replyStatus == ReplyStatus.READY_NOT_READ) { showDialog = true } } - Scaffold( topBar = { val month = replyDiaryState.month @@ -125,7 +124,7 @@ fun ReplyDiaryScreen( ) }, navigationIcon = { - IconButton(onClick = onClickBack) { + IconButton(onClick = navigateToHome) { Image( painterResource(id = R.drawable.ic_nickname_back), contentDescription = "back", From f470601d8757a6c25153325bd36a4673e865f4ce Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:26:28 +0900 Subject: [PATCH 029/299] =?UTF-8?q?[REFACTOR/#260]=20=ED=99=88=20=EB=84=A4?= =?UTF-8?q?=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC=EC=A1=B0=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/home/navigation/HomeNavGraph.kt | 29 ------------ .../ui/home/navigation/HomeNavigation.kt | 47 +++++++++++++++++++ .../ui/home/navigation/HomeNavigator.kt | 27 ----------- 3 files changed, 47 insertions(+), 56 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavGraph.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigator.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavGraph.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavGraph.kt deleted file mode 100644 index 3ef284b8..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavGraph.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.sopt.clody.presentation.ui.home.navigation - -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import com.sopt.clody.presentation.ui.home.screen.HomeRoute -import java.time.LocalDate - -fun NavGraphBuilder.homeNavGraph( - navigator: HomeNavigator, -) { - composable( - route = "home/{selectedYear}/{selectedMonth}", - arguments = listOf( - navArgument("selectedYear") { type = NavType.IntType }, - navArgument("selectedMonth") { type = NavType.IntType }, - ), - ) { backStackEntry -> - val currentDate = LocalDate.now() - val selectedYear = backStackEntry.arguments?.getInt("selectedYear") ?: currentDate.year - val selectedMonth = backStackEntry.arguments?.getInt("selectedMonth") ?: currentDate.monthValue - HomeRoute( - navigator = navigator, - selectedYear = selectedYear, - selectedMonth = selectedMonth, - ) - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt new file mode 100644 index 00000000..1d002b15 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt @@ -0,0 +1,47 @@ +package com.sopt.clody.presentation.ui.home.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.sopt.clody.presentation.ui.home.screen.HomeRoute +import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.presentation.utils.navigation.Route +import java.time.LocalDate + +fun NavGraphBuilder.homeScreen( + navigateToDiaryList: (year: Int, month: Int) -> Unit, + navigateToSetting: () -> Unit, + navigateToWriteDiary: (year: Int, month: Int, date: Int) -> Unit, + navigateToReplyLoading: ( + year: Int, + month: Int, + date: Int, + from: Route.ReplyLoading.ReplyLoadingFrom, + replyStatus: ReplyStatus, + ) -> Unit, +) { + composable { backStackEntry -> + backStackEntry.toRoute().apply { + HomeRoute( + selectedYear = selectedYear, + selectedMonth = selectedMonth, + selectedDay = selectedDay, + navigateToDiaryList = navigateToDiaryList, + navigateToSetting = navigateToSetting, + navigateToWriteDiary = navigateToWriteDiary, + navigateToReplyLoading = navigateToReplyLoading, + ) + } + } +} + +fun NavController.navigateToHome( + selectedYear: Int = LocalDate.now().year, + selectedMonth: Int = LocalDate.now().monthValue, + selectedDay: Int? = LocalDate.now().dayOfMonth, + navOptions: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(Route.Home(selectedYear, selectedMonth, selectedDay), navOptions) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigator.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigator.kt deleted file mode 100644 index 80428b26..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigator.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.sopt.clody.presentation.ui.home.navigation - -import androidx.navigation.NavController - -class HomeNavigator( - val navController: NavController, -) { - fun navigateDiaryList(selectedYear: Int, selectedMonth: Int) { - navController.navigate("diary_list/$selectedYear/$selectedMonth") - } - - fun navigateSetting() { - navController.navigate("setting") - } - - fun navigateWriteDiary(year: Int, month: Int, day: Int) { - navController.navigate("write_diary/$year/$month/$day") - } - - fun navigateReplyLoading(year: Int, month: Int, day: Int, replyStatus: String) { - navController.navigate("reply_loading/$year/$month/$day?from=home&replyStatus=$replyStatus") - } - - fun navigateBack() { - navController.navigateUp() - } -} From 799bbd84a4d36c8fa51151b9021fd66dc35610a9 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:27:11 +0900 Subject: [PATCH 030/299] =?UTF-8?q?[REFACTOR/#260]=20=ED=99=88=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=83=81=ED=83=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/home/screen/HomeScreen.kt | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 1e9ef2c9..1271ae2a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -29,20 +29,33 @@ import com.sopt.clody.presentation.ui.component.timepicker.YearMonthPicker import com.sopt.clody.presentation.ui.home.component.DiaryStateButton import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar import com.sopt.clody.presentation.ui.home.model.DiaryDateData -import com.sopt.clody.presentation.ui.home.navigation.HomeNavigator import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme +import java.time.LocalDate @Composable fun HomeRoute( - navigator: HomeNavigator, - homeViewModel: HomeViewModel = hiltViewModel(), selectedYear: Int, selectedMonth: Int, + selectedDay: Int?, + navigateToDiaryList: (year: Int, month: Int) -> Unit, + navigateToSetting: () -> Unit, + navigateToWriteDiary: (year: Int, month: Int, date: Int) -> Unit, + navigateToReplyLoading: ( + year: Int, + month: Int, + date: Int, + from: Route.ReplyLoading.ReplyLoadingFrom, + replyStatus: ReplyStatus, + ) -> Unit, + homeViewModel: HomeViewModel = hiltViewModel(), ) { val calendarState by homeViewModel.calendarState.collectAsStateWithLifecycle() val dailyDiariesState by homeViewModel.dailyDiariesState.collectAsStateWithLifecycle() + val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() val isError = calendarState is CalendarState.Error || dailyDiariesState is DailyDiariesState.Error val errorMessage = when { @@ -55,10 +68,17 @@ fun HomeRoute( AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) } - LaunchedEffect(selectedYear, selectedMonth) { + LaunchedEffect(selectedYear, selectedMonth, selectedDay) { homeViewModel.refreshCalendarDataCalendarData(selectedYear, selectedMonth) - val selectedDate = homeViewModel.selectedDate.value - homeViewModel.loadDailyDiariesData(selectedYear, selectedMonth, selectedDate.dayOfMonth) + + if (selectedDay != null) { + homeViewModel.updateSelectedDate(LocalDate.of(selectedYear, selectedMonth, selectedDay)) + homeViewModel.loadDailyDiariesData(selectedYear, selectedMonth, selectedDay) + } else { + val today = LocalDate.now() + homeViewModel.updateSelectedDate(today) + homeViewModel.loadDailyDiariesData(today.year, today.monthValue, today.dayOfMonth) + } } if (isError) { @@ -73,20 +93,21 @@ fun HomeRoute( } else { HomeScreen( homeViewModel = homeViewModel, - onClickDiaryList = { selectedYearFromHome, selectedMonthFromHome -> - navigator.navigateDiaryList( - selectedYearFromHome, - selectedMonthFromHome, - ) - }, - onClickSetting = { navigator.navigateSetting() }, + onClickDiaryList = navigateToDiaryList, + onClickSetting = navigateToSetting, onClickWriteDiary = { year, month, day -> AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_WRITING_DIARY) - navigator.navigateWriteDiary(year, month, day) + navigateToWriteDiary(year, month, day) }, - onClickReplyDiary = { year, month, day, replyStatus -> + onClickReplyDiary = { year, month, day, _ -> AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_REPLY) - navigator.navigateReplyLoading(year, month, day, replyStatus) + navigateToReplyLoading( + year, + month, + day, + Route.ReplyLoading.ReplyLoadingFrom.HOME, + ReplyStatus.valueOf(replyStatus), + ) }, selectedYear = selectedYear, selectedMonth = selectedMonth, @@ -100,7 +121,12 @@ fun HomeScreen( onClickDiaryList: (Int, Int) -> Unit, onClickSetting: () -> Unit, onClickWriteDiary: (Int, Int, Int) -> Unit, - onClickReplyDiary: (Int, Int, Int, String) -> Unit, + onClickReplyDiary: ( + year: Int, + month: Int, + date: Int, + replyStatus: Route.ReplyLoading.ReplyLoadingFrom, + ) -> Unit, selectedYear: Int, selectedMonth: Int, ) { @@ -218,7 +244,7 @@ fun HomeScreen( selectedDate.year, selectedDate.monthValue, selectedDate.dayOfMonth, - replyStatus, + Route.ReplyLoading.ReplyLoadingFrom.HOME, ) }, ) From 633ad43e0b87874eb8bd8de6556d8e36a2150cf1 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:27:24 +0900 Subject: [PATCH 031/299] =?UTF-8?q?[REFACTOR/#260]=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=84=A4=EB=B9=84=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../diarylist/navigation/DiaryListNavGraph.kt | 29 ----------------- .../navigation/DiaryListNavigation.kt | 31 +++++++++++++++++++ .../navigation/DiaryListNavigator.kt | 19 ------------ 3 files changed, 31 insertions(+), 48 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavGraph.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigator.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavGraph.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavGraph.kt deleted file mode 100644 index 46ef810d..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavGraph.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.sopt.clody.presentation.ui.diarylist.navigation - -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.navArgument -import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListRoute -import java.time.LocalDate - -fun NavGraphBuilder.diaryListNavGraph( - diaryListNavigator: DiaryListNavigator, -) { - composable( - route = "diary_list/{selectedYearFromHome}/{selectedMonthFromHome}", - arguments = listOf( - navArgument("selectedYearFromHome") { type = NavType.IntType }, - navArgument("selectedMonthFromHome") { type = NavType.IntType }, - ), - ) { backStackEntry -> - val currentDate = LocalDate.now() - val selectedYearFromHome = backStackEntry.arguments?.getInt("selectedYearFromHome") ?: currentDate.year - val selectedMonthFromHome = backStackEntry.arguments?.getInt("selectedMonthFromHome") ?: currentDate.monthValue - DiaryListRoute( - navigator = diaryListNavigator, - selectedYearFromHome = selectedYearFromHome, - selectedMonthFromHome = selectedMonthFromHome, - ) - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt new file mode 100644 index 00000000..add30750 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt @@ -0,0 +1,31 @@ +package com.sopt.clody.presentation.ui.diarylist.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListRoute +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.diaryListScreen( + navigateToHome: (year: Int, month: Int) -> Unit, + navigateToReplyLoading: (year: Int, month: Int, date: Int, replyStatus: Route.ReplyLoading.ReplyLoadingFrom) -> Unit, +) { + composable { backStackEntry -> + backStackEntry.toRoute().apply { + DiaryListRoute( + selectedYearFromHome = selectedYearFromHome, + selectedMonthFromHome = selectedMonthFromHome, + navigateToHome = navigateToHome, + navigateToReplyLoading = navigateToReplyLoading, + ) + } + } +} + +fun NavController.navigateToDiaryList( + selectedYearFromHome: Int, + selectedMonthFromHome: Int, +) { + navigate(Route.DiaryList(selectedYearFromHome, selectedMonthFromHome)) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigator.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigator.kt deleted file mode 100644 index c3546037..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigator.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.sopt.clody.presentation.ui.diarylist.navigation - -import androidx.navigation.NavController - -class DiaryListNavigator( - val navController: NavController, -) { - fun navigateHome(selectedYear: Int, selectedMonth: Int) { - navController.navigate("home/$selectedYear/$selectedMonth") - } - - fun navigateReplyLoading(year: Int, month: Int, day: Int, replyStatus: String) { - navController.navigate("reply_loading/$year/$month/$day?from=diary_list&replyStatus=$replyStatus") - } - - fun navigateBack() { - navController.navigateUp() - } -} From a565415e0bc580ae843b0b4096ab49c799a95d8f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:27:57 +0900 Subject: [PATCH 032/299] =?UTF-8?q?[REFACTOR/#260]=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=9D=B8=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/diarylist/screen/DiaryListScreen.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt index 086e56a0..8e35a464 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt @@ -22,17 +22,18 @@ import com.sopt.clody.presentation.ui.component.timepicker.YearMonthPicker import com.sopt.clody.presentation.ui.diarylist.component.DiaryListTopAppBar import com.sopt.clody.presentation.ui.diarylist.component.EmptyDiaryList import com.sopt.clody.presentation.ui.diarylist.component.MonthlyDiaryList -import com.sopt.clody.presentation.ui.diarylist.navigation.DiaryListNavigator import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme @Composable fun DiaryListRoute( - navigator: DiaryListNavigator, - diaryListViewModel: DiaryListViewModel = hiltViewModel(), selectedYearFromHome: Int, selectedMonthFromHome: Int, + navigateToHome: (Int, Int) -> Unit, + navigateToReplyLoading: (year: Int, month: Int, date: Int, replyStatus: Route.ReplyLoading.ReplyLoadingFrom) -> Unit, + diaryListViewModel: DiaryListViewModel = hiltViewModel(), ) { var selectedYearInDiaryList by remember { mutableIntStateOf(selectedYearFromHome) } var selectedMonthInDiaryList by remember { mutableIntStateOf(selectedMonthFromHome) } @@ -81,10 +82,11 @@ fun DiaryListRoute( }, dismissDiaryDeleteDialog = { diaryDeleteDialogState = false }, onClickDiaryDelete = { year, month, day -> diaryListViewModel.deleteDailyDiary(year, month, day) }, - onClickCalendar = { navigator.navigateHome(selectedYearInDiaryList, selectedMonthInDiaryList) }, - onClickReplyDiary = { year, month, day, replyStatus -> - navigator.navigateReplyLoading(year, month, day, replyStatus) - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.LIST_REPLY) + onClickCalendar = { + navigateToHome(selectedYearInDiaryList, selectedMonthInDiaryList) + }, + onClickReplyDiary = { year, month, day, _ -> + navigateToReplyLoading(year, month, day, Route.ReplyLoading.ReplyLoadingFrom.DIARY_LIST) }, ) } From 4e9df39144696f8a3b2f45192cd41dd6522f22e1 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 7 May 2025 01:28:18 +0900 Subject: [PATCH 033/299] =?UTF-8?q?[MOVE/#260]=20=EB=84=A4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=9E=84=ED=8F=AC=ED=8A=B8=20=EA=B2=BD=EB=A1=9C=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 --- .../java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt index a8c43e60..cc0ef3c3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt @@ -7,8 +7,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost -import com.sopt.clody.presentation.ui.auth.guide.guideScreen -import com.sopt.clody.presentation.ui.auth.guide.navigateToGuide +import com.sopt.clody.presentation.ui.auth.guide.navigation.guideScreen +import com.sopt.clody.presentation.ui.auth.guide.navigation.navigateToGuide import com.sopt.clody.presentation.ui.auth.login.navigation.loginScreen import com.sopt.clody.presentation.ui.auth.login.navigation.navigateToLogin import com.sopt.clody.presentation.ui.auth.signup.navigation.navigateToNickname From 7045a8e26fd11e58553b0e3571e458ee405e2af5 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Fri, 23 May 2025 18:12:04 +0900 Subject: [PATCH 034/299] =?UTF-8?q?[REFACTOR/#260]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=EB=82=B4=20=EC=95=BD=EA=B4=80?= =?UTF-8?q?=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=9D=B8?= =?UTF-8?q?=EC=9E=90=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 --- .../com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt | 4 ++-- .../presentation/ui/auth/login/navigation/LoginNavigation.kt | 4 ++-- .../java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt index 96542b5f..edd916bd 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt @@ -33,7 +33,7 @@ import com.sopt.clody.ui.theme.ClodyTheme @Composable fun LoginRoute( - navigateToTerms: () -> Unit, + navigateToTermsOfService: () -> Unit, navigateToHome: () -> Unit, ) { val viewModel: SignUpViewModel = hiltViewModel() @@ -43,7 +43,7 @@ fun LoginRoute( LaunchedEffect(signInState.uiState) { when (signInState.uiState) { is UiState.Success -> navigateToHome() - is UiState.Failure -> navigateToTerms() + is UiState.Failure -> navigateToTermsOfService() else -> Unit } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt index 9d728d7a..2eff62a1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt @@ -8,12 +8,12 @@ import com.sopt.clody.presentation.ui.auth.login.LoginRoute import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.loginScreen( - navigateToTerms: () -> Unit, + navigateToTermsOfService: () -> Unit, navigateToHome: () -> Unit, ) { composable { LoginRoute( - navigateToTerms = navigateToTerms, + navigateToTermsOfService = navigateToTermsOfService, navigateToHome = navigateToHome, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt index cc0ef3c3..68f732c7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt @@ -69,7 +69,7 @@ fun ClodyNavHost( ) loginScreen( - navigateToTerms = navController::navigateToTermsOfService, + navigateToTermsOfService = navController::navigateToTermsOfService, navigateToHome = navController::navigateToHome, ) termsOfServiceScreen( From af4805688b1f83e1c106bd03c76b6c78e6e2d3b9 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Fri, 23 May 2025 18:12:12 +0900 Subject: [PATCH 035/299] =?UTF-8?q?[REFACTOR/#260]=20SignUpNavigation?= =?UTF-8?q?=EC=97=90=EC=84=9C=20NavHostController=EB=A5=BC=20NavController?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/auth/signup/navigation/SignUpNavigation.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt index da91b80e..35b29501 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt @@ -2,7 +2,6 @@ package com.sopt.clody.presentation.ui.auth.signup.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavHostController import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import com.sopt.clody.presentation.ui.auth.signup.NicknameRoute @@ -33,7 +32,7 @@ fun NavGraphBuilder.nicknameScreen( } } -fun NavHostController.navigateToNickname( +fun NavController.navigateToNickname( navOptions: NavOptionsBuilder.() -> Unit = {}, ) { navigate(Route.Nickname, navOptions) From 1166d54d61d8b521503b63333db1c37dfe778e53 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Fri, 23 May 2025 18:13:29 +0900 Subject: [PATCH 036/299] =?UTF-8?q?[REFACTOR/#260]=20=EB=82=B4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EB=A7=A4=EA=B0=9C=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=9D=B4=EB=A6=84=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/diarylist/screen/DiaryListScreen.kt | 2 +- .../ui/replyloading/navigation/ReplyLoadingNavigation.kt | 4 ++-- .../ui/replyloading/screen/ReplyLoadingScreen.kt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt index 8e35a464..6522156d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt @@ -31,7 +31,7 @@ import com.sopt.clody.ui.theme.ClodyTheme fun DiaryListRoute( selectedYearFromHome: Int, selectedMonthFromHome: Int, - navigateToHome: (Int, Int) -> Unit, + navigateToHome: (year: Int, month: Int) -> Unit, navigateToReplyLoading: (year: Int, month: Int, date: Int, replyStatus: Route.ReplyLoading.ReplyLoadingFrom) -> Unit, diaryListViewModel: DiaryListViewModel = hiltViewModel(), ) { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt index e793b3c0..aa42a929 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt @@ -10,9 +10,9 @@ import com.sopt.clody.presentation.utils.navigation.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.replyLoadingScreen( - navigateToReplyDiary: (Int, Int, Int, ReplyStatus) -> Unit, + navigateToReplyDiary: (year: Int, month: Int, day: Int, status: ReplyStatus) -> Unit, navigateToHome: (year: Int, month: Int, day: Int) -> Unit, - navigateToDiaryList: (Int, Int) -> Unit, + navigateToDiaryList: (year: Int, month: Int) -> Unit, ) { composable { backStackEntry -> backStackEntry.toRoute().apply { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt index f9d0e939..a3fd0b64 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt @@ -59,9 +59,9 @@ fun ReplyLoadingRoute( date: Int, from: Route.ReplyLoading.ReplyLoadingFrom, replyStatus: ReplyStatus, - navigateToReplyDiary: (Int, Int, Int, ReplyStatus) -> Unit, - navigateToHome: (Int, Int, Int) -> Unit, - navigateToDiaryList: (Int, Int) -> Unit, + navigateToReplyDiary: (year: Int, month: Int, day: Int, status: ReplyStatus) -> Unit, + navigateToHome: (year: Int, month: Int, day: Int) -> Unit, + navigateToDiaryList: (year: Int, month: Int) -> Unit, viewModel: ReplyLoadingViewModel = hiltViewModel(), ) { val replyLoadingState by viewModel.replyLoadingState.collectAsState() From 308a9bb5365481f94dab34ba9f85af93b671aae9 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Fri, 23 May 2025 18:13:51 +0900 Subject: [PATCH 037/299] =?UTF-8?q?[ADD/#260]=20Clover=20Dialog=EB=A5=BC?= =?UTF-8?q?=20=EB=9D=84=EC=9A=B0=EB=8A=94=20=EC=83=81=ED=83=9C=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 --- .../sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt index b83c8a01..75d92bd1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt @@ -106,7 +106,7 @@ fun ReplyDiaryScreen( var showDialog by remember { mutableStateOf(false) } LaunchedEffect(replyDiaryState) { - if (replyStatus == ReplyStatus.READY_NOT_READ) { + if (replyStatus == ReplyStatus.READY_NOT_READ || replyStatus == ReplyStatus.UNREADY) { showDialog = true } } From 9adfea26b03157bee9acdc5fe0e9b6f1ab1f336c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 25 May 2025 16:37:58 +0900 Subject: [PATCH 038/299] =?UTF-8?q?[REFACTOR/#260]=20DailyDiary=EC=9D=98?= =?UTF-8?q?=20replyStatus=20=ED=83=80=EC=9E=85=EC=9D=84=20ReplyStatus=20en?= =?UTF-8?q?um=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt index 27b0883a..35533c1d 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt @@ -1,5 +1,6 @@ package com.sopt.clody.data.remote.dto.response +import com.sopt.clody.presentation.utils.navigation.ReplyStatus import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -11,7 +12,7 @@ data class MonthlyDiaryResponseDto( @Serializable data class DailyDiary( @SerialName("diaryCount") val diaryCount: Int, - @SerialName("replyStatus") val replyStatus: String, + @SerialName("replyStatus") val replyStatus: ReplyStatus, @SerialName("date") val date: String, @SerialName("diary") val diary: List, @SerialName("isDeleted") val isDeleted: Boolean, From cb09b1edc48cc7289792f2d954c44f544950e0ef Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 25 May 2025 16:41:08 +0900 Subject: [PATCH 039/299] =?UTF-8?q?[REFACTOR/#260]=20diary=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20replyStatus=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=9D=84=20ReplyStatus=20enum=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/diarylist/component/DailyDiaryCard.kt | 11 +++++----- .../diarylist/component/MonthlyDiaryList.kt | 3 ++- .../navigation/DiaryListNavigation.kt | 9 +++++++- .../ui/diarylist/screen/DiaryListScreen.kt | 21 +++++++++++++++---- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt index 63e10649..df96bff7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel +import com.sopt.clody.presentation.utils.navigation.ReplyStatus import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -41,11 +42,11 @@ fun DailyDiaryCard( day: Int, dayOfWeek: String, showDiaryDeleteBottomSheet: () -> Unit, - onClickReplyDiary: (Int, Int, Int, String) -> Unit, + onClickReplyDiary: (Int, Int, Int, ReplyStatus) -> Unit, ) { val iconRes = when { - dailyDiary.replyStatus == "READY_NOT_READ" && dailyDiary.diaryCount > 0 -> R.drawable.ic_home_ungiven_clover - dailyDiary.replyStatus == "UNREADY" && dailyDiary.diaryCount > 0 -> R.drawable.ic_home_ungiven_clover + dailyDiary.replyStatus == ReplyStatus.READY_NOT_READ && dailyDiary.diaryCount > 0 -> R.drawable.ic_home_ungiven_clover + dailyDiary.replyStatus == ReplyStatus.UNREADY && dailyDiary.diaryCount > 0 -> R.drawable.ic_home_ungiven_clover dailyDiary.diaryCount == 0 -> R.drawable.ic_home_ungiven_clover dailyDiary.diaryCount in 1..2 -> R.drawable.ic_home_bottom_clover dailyDiary.diaryCount in 3..4 -> R.drawable.ic_home_mid_clover @@ -120,7 +121,7 @@ fun ReplyDiaryButton( year: Int, month: Int, day: Int, - onClickReplyDiary: (Int, Int, Int, String) -> Unit, + onClickReplyDiary: (Int, Int, Int, ReplyStatus) -> Unit, ) { Box( contentAlignment = Alignment.TopEnd, @@ -147,7 +148,7 @@ fun ReplyDiaryButton( style = ClodyTheme.typography.detail1SemiBold, ) } - if (dailyDiary.replyStatus == "READY_NOT_READ") { + if (dailyDiary.replyStatus == ReplyStatus.READY_NOT_READ) { Image( painter = painterResource(id = R.drawable.ic_reply_diary_new), modifier = Modifier diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt index 49f8bdbc..aa7f4c29 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.unit.dp import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel import com.sopt.clody.presentation.utils.extension.getDayOfWeek +import com.sopt.clody.presentation.utils.navigation.ReplyStatus @Composable fun MonthlyDiaryList( @@ -18,7 +19,7 @@ fun MonthlyDiaryList( diaryListViewModel: DiaryListViewModel, diaries: List, showDiaryDeleteBottomSheet: () -> Unit, - onClickReplyDiary: (Int, Int, Int, String) -> Unit, + onClickReplyDiary: (Int, Int, Int, ReplyStatus) -> Unit, ) { LazyColumn( modifier = Modifier diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt index add30750..3b5c1235 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt @@ -5,11 +5,18 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListRoute +import com.sopt.clody.presentation.utils.navigation.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.diaryListScreen( navigateToHome: (year: Int, month: Int) -> Unit, - navigateToReplyLoading: (year: Int, month: Int, date: Int, replyStatus: Route.ReplyLoading.ReplyLoadingFrom) -> Unit, + navigateToReplyLoading: ( + year: Int, + month: Int, + date: Int, + from: Route.ReplyLoading.ReplyLoadingFrom, + replyStatus: ReplyStatus, + ) -> Unit, ) { composable { backStackEntry -> backStackEntry.toRoute().apply { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt index 6522156d..78b5edf9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt @@ -24,6 +24,7 @@ import com.sopt.clody.presentation.ui.diarylist.component.EmptyDiaryList import com.sopt.clody.presentation.ui.diarylist.component.MonthlyDiaryList import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.navigation.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme @@ -32,7 +33,13 @@ fun DiaryListRoute( selectedYearFromHome: Int, selectedMonthFromHome: Int, navigateToHome: (year: Int, month: Int) -> Unit, - navigateToReplyLoading: (year: Int, month: Int, date: Int, replyStatus: Route.ReplyLoading.ReplyLoadingFrom) -> Unit, + navigateToReplyLoading: ( + year: Int, + month: Int, + date: Int, + from: Route.ReplyLoading.ReplyLoadingFrom, + replyStatus: ReplyStatus, + ) -> Unit, diaryListViewModel: DiaryListViewModel = hiltViewModel(), ) { var selectedYearInDiaryList by remember { mutableIntStateOf(selectedYearFromHome) } @@ -85,8 +92,14 @@ fun DiaryListRoute( onClickCalendar = { navigateToHome(selectedYearInDiaryList, selectedMonthInDiaryList) }, - onClickReplyDiary = { year, month, day, _ -> - navigateToReplyLoading(year, month, day, Route.ReplyLoading.ReplyLoadingFrom.DIARY_LIST) + onClickReplyDiary = { year, month, day, replyStatus -> + navigateToReplyLoading( + year, + month, + day, + Route.ReplyLoading.ReplyLoadingFrom.DIARY_LIST, + replyStatus, + ) }, ) } @@ -112,7 +125,7 @@ fun DiaryListScreen( dismissDiaryDeleteDialog: () -> Unit, onClickDiaryDelete: (Int, Int, Int) -> Unit, onClickCalendar: () -> Unit, - onClickReplyDiary: (Int, Int, Int, String) -> Unit, + onClickReplyDiary: (Int, Int, Int, ReplyStatus) -> Unit, ) { Scaffold( topBar = { From 8bbb1f73938d0bf0946f06094fc2651abc8886bc Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:11:02 +0900 Subject: [PATCH 040/299] =?UTF-8?q?[ADD/#252]=20Mavericks=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 3 +++ gradle/libs.versions.toml | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5c3ff226..2eafa6da 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -120,6 +120,9 @@ dependencies { implementation(libs.accompanist.systemuicontroller) implementation(libs.accompanist.insets) + // Mavericks + implementation(libs.bundles.mavericks) + // ETC implementation(libs.timber) implementation(libs.lottie.compose) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 134ae3ba..bb253a92 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,6 +48,8 @@ accompanist-insets = "0.28.0" firebase-config-ktx = "22.1.0" +mavericks = "3.0.9" + [libraries] # AndroidX Core core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } @@ -115,6 +117,11 @@ admob = { group = "com.google.android.gms", name = "play-services-ads", version. accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } accompanist-insets = { group = "com.google.accompanist", name = "accompanist-insets", version.ref = "accompanist-insets" } +# Mavericks +mavericks = { module = "com.airbnb.android:mavericks", version.ref = "mavericks" } +mavericks-compose = { module = "com.airbnb.android:mavericks-compose", version.ref = "mavericks" } +mavericks-hilt = { module = "com.airbnb.android:mavericks-hilt", version.ref = "mavericks" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } @@ -180,3 +187,9 @@ firebase = [ "firebase-analytics", "firebase-crashlytics" ] + +mavericks = [ + "mavericks", + "mavericks-compose", + "mavericks-hilt" +] From 687a74ffb9443180b5bf4a22f9f6c79bc454035d Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:11:14 +0900 Subject: [PATCH 041/299] =?UTF-8?q?[ADD/#252]=20Mavericks=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/sopt/clody/ClodyApplication.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/ClodyApplication.kt b/app/src/main/java/com/sopt/clody/ClodyApplication.kt index a73118f9..def59341 100644 --- a/app/src/main/java/com/sopt/clody/ClodyApplication.kt +++ b/app/src/main/java/com/sopt/clody/ClodyApplication.kt @@ -1,6 +1,7 @@ package com.sopt.clody import android.app.Application +import com.airbnb.mvrx.Mavericks import com.google.firebase.FirebaseApp import com.kakao.sdk.common.KakaoSdk import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils.initAmplitude @@ -14,6 +15,7 @@ class ClodyApplication : Application() { Timber.plant(Timber.DebugTree()) initKakaoSdk() FirebaseApp.initializeApp(this) + Mavericks.initialize(this) initAmplitude(applicationContext) } From 0c45ebc94e982897f9f26015abb29399966bac01 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:14:46 +0900 Subject: [PATCH 042/299] =?UTF-8?q?[ADD/#252]=20LifecycleOwner=20repeatOnS?= =?UTF-8?q?tarted=20=ED=99=95=EC=9E=A5=20=ED=95=A8=EC=88=98=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 --- .../presentation/utils/extension/LifeCycle.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt new file mode 100644 index 00000000..61458078 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt @@ -0,0 +1,27 @@ +package com.sopt.clody.presentation.utils.extension + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * [LifecycleOwner]의 생명주기가 [Lifecycle.State.STARTED] 상태 이상일 때만 + * 지정한 [block]을 실행하고, 그렇지 않으면 자동으로 중단. + * + * 내부적으로 [lifecycleScope]에서 코루틴을 실행하고, + * [Lifecycle.repeatOnLifecycle]을 STARTED 상태 기준으로 래핑. + * + * @param block STARTED 상태에서 실행할 suspend 함수 블록 + * + * @see Lifecycle.repeatOnLifecycle + * @see Lifecycle.State.STARTED + */ + +fun LifecycleOwner.repeatOnStarted(block: suspend CoroutineScope.() -> Unit) { + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block) + } +} From 3b9694c660bcc8c04a4aea370a15080ab549c1fb Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:17:18 +0900 Subject: [PATCH 043/299] =?UTF-8?q?[ADD/#252]=20BasePreview=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EC=A0=80=EB=B8=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/utils/base/ClodyPreview.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/base/ClodyPreview.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/base/ClodyPreview.kt b/app/src/main/java/com/sopt/clody/presentation/utils/base/ClodyPreview.kt new file mode 100644 index 00000000..c2315295 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/base/ClodyPreview.kt @@ -0,0 +1,23 @@ +package com.sopt.clody.presentation.utils.base + +import android.content.res.Configuration +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import com.sopt.clody.ui.theme.ClodyTheme + +// 아래 폴드는 예시이고 fontScale 같은 값도 조정이 가능합니다. +//@Preview(name = "Galaxy Z Fold3 접힌화면 (840x2289)", widthDp = 320, heightDp = 870, showBackground = true) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@PreviewScreenSizes +annotation class ClodyPreview + +@Composable +fun BasePreview(content: @Composable () -> Unit = {}) { + ClodyTheme { + Surface(color = ClodyTheme.colors.white) { + content() + } + } +} From 26b2bf5fc5d6065863c8f3c7ba74ccc02ebc1110 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:20:42 +0900 Subject: [PATCH 044/299] =?UTF-8?q?[REFACTOR/#252]=20Splash=EC=97=90=20Mav?= =?UTF-8?q?ericks=20Mvi=20=EA=B5=AC=EC=A1=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/splash/SplashContract.kt | 23 +++++ .../presentation/ui/splash/SplashScreen.kt | 53 +++++----- .../presentation/ui/splash/SplashViewModel.kt | 96 ++++++++++++++----- 3 files changed, 120 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt new file mode 100644 index 00000000..f6a2bd4d --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt @@ -0,0 +1,23 @@ +package com.sopt.clody.presentation.ui.splash + +import android.content.Intent +import com.airbnb.mvrx.MavericksState +import com.sopt.clody.domain.model.AppUpdateState + +class SplashContract { + + data class SplashState( + val isUserLoggedIn: Boolean? = null, + val updateState: AppUpdateState? = null, + ) : MavericksState + + sealed class SplashIntent { + data class InitSplash(val startIntent: Intent) : SplashIntent() + data object ClearUpdateState : SplashIntent() + } + + sealed interface SplashSideEffect { + data object NavigateToLogin : SplashSideEffect + data object NavigateToHome : SplashSideEffect + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt index 94fdc967..ac080703 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt @@ -18,60 +18,55 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource 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.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.airbnb.mvrx.compose.collectAsState +import com.airbnb.mvrx.compose.mavericksViewModel import com.sopt.clody.R import com.sopt.clody.domain.model.AppUpdateState -import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints -import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.appupdate.AppUpdateUtils +import com.sopt.clody.presentation.utils.base.BasePreview +import com.sopt.clody.presentation.utils.base.ClodyPreview +import com.sopt.clody.presentation.utils.extension.repeatOnStarted import com.sopt.clody.ui.theme.ClodyTheme -import kotlinx.coroutines.delay @Composable fun SplashRoute( + viewModel: SplashViewModel = mavericksViewModel(), startIntent: Intent, onLoginRequired: () -> Unit, onAlreadyLoggedIn: () -> Unit, - viewModel: SplashViewModel = hiltViewModel(), ) { - val isUserLoggedIn by viewModel.isUserLoggedIn.collectAsStateWithLifecycle() - val updateState by viewModel.updateState.collectAsStateWithLifecycle() + val state by viewModel.collectAsState() val context = LocalContext.current val activity = context as Activity + val lifecycleOwner = LocalLifecycleOwner.current - // Push 클릭 추적 - LaunchedEffect(startIntent) { - if (startIntent.hasExtra("google.message_id")) { - AmplitudeUtils.trackEvent(AmplitudeConstraints.ALARM) - } - } + LaunchedEffect(viewModel) { + viewModel.postIntent(SplashContract.SplashIntent.InitSplash(startIntent)) - LaunchedEffect(isUserLoggedIn, updateState) { - if (isUserLoggedIn != null && updateState == AppUpdateState.Latest) { - delay(1000) - if (isUserLoggedIn == true) { - onAlreadyLoggedIn() - } else { - onLoginRequired() + lifecycleOwner.repeatOnStarted { + viewModel.sideEffects.collect { effect -> + when (effect) { + is SplashContract.SplashSideEffect.NavigateToLogin -> onLoginRequired() + is SplashContract.SplashSideEffect.NavigateToHome -> onAlreadyLoggedIn() + } } } } - when (val state = updateState) { + when (val updateState = state.updateState) { is AppUpdateState.SoftUpdate -> { SoftUpdateDialog( - latestVersion = state.latestVersion, - onDismiss = { viewModel.clearUpdateState() }, + latestVersion = updateState.latestVersion, + onDismiss = { viewModel.postIntent(SplashContract.SplashIntent.ClearUpdateState) }, onConfirm = { AppUpdateUtils.navigateToMarket(context) }, ) } is AppUpdateState.HardUpdate -> { HardUpdateDialog( - latestVersion = state.latestVersion, + latestVersion = updateState.latestVersion, onConfirm = { AppUpdateUtils.navigateToMarketAndFinish(activity) }, onExit = { activity.finishAffinity() }, ) @@ -135,8 +130,10 @@ fun HardUpdateDialog( ) } -@Preview(showBackground = true) +@ClodyPreview @Composable fun SplashScreenPreview() { - SplashScreen() + BasePreview { + SplashScreen() + } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt index b52627bb..d8c3b746 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt @@ -1,48 +1,96 @@ package com.sopt.clody.presentation.ui.splash -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.hilt.AssistedViewModelFactory +import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory +import com.airbnb.mvrx.withState import com.sopt.clody.BuildConfig import com.sopt.clody.domain.appupdate.AppUpdateChecker import com.sopt.clody.domain.model.AppUpdateState import com.sopt.clody.domain.repository.TokenRepository -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints +import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import javax.inject.Inject -@HiltViewModel -class SplashViewModel @Inject constructor( +class SplashViewModel @AssistedInject constructor( + @Assisted initialState: SplashContract.SplashState, private val tokenRepository: TokenRepository, private val appUpdateChecker: AppUpdateChecker, -) : ViewModel() { +) : MavericksViewModel(initialState) { - private val _isUserLoggedIn = MutableStateFlow(null) - val isUserLoggedIn: StateFlow = _isUserLoggedIn - - private val _updateState = MutableStateFlow(null) - val updateState: StateFlow = _updateState + private val _intents = Channel(BUFFERED) + private val _sideEffects = Channel(BUFFERED) + val sideEffects = _sideEffects.receiveAsFlow() init { - attemptAutoLogin() - checkVersion() + _intents + .receiveAsFlow() + .onEach(::handleIntent) + .launchIn(viewModelScope) + } + + fun postIntent(intent: SplashContract.SplashIntent) { + viewModelScope.launch { _intents.send(intent) } + } + + private suspend fun handleIntent(intent: SplashContract.SplashIntent) { + when (intent) { + is SplashContract.SplashIntent.InitSplash -> { + if (intent.startIntent.hasExtra("google.message_id")) { + AmplitudeUtils.trackEvent(AmplitudeConstraints.ALARM) + } + attemptAutoLogin() + checkVersionAndNavigate() + } + + SplashContract.SplashIntent.ClearUpdateState -> { + setState { copy(updateState = AppUpdateState.Latest) } + } + } } private fun attemptAutoLogin() { - val accessToken = tokenRepository.getAccessToken() - val refreshToken = tokenRepository.getRefreshToken() - _isUserLoggedIn.value = accessToken.isNotBlank() && refreshToken.isNotBlank() + val isLoggedIn = tokenRepository.getAccessToken().isNotBlank() && + tokenRepository.getRefreshToken().isNotBlank() + setState { copy(isUserLoggedIn = isLoggedIn) } } - private fun checkVersion() { + private fun checkVersionAndNavigate() { viewModelScope.launch { - val state = appUpdateChecker.getAppUpdateState(BuildConfig.VERSION_NAME) - _updateState.value = state + val updateState = appUpdateChecker.getAppUpdateState(BuildConfig.VERSION_NAME) + setState { copy(updateState = updateState) } + + if (updateState == AppUpdateState.Latest) { + delay(1000) + + val isLoggedIn = withState(this@SplashViewModel) { + it.isUserLoggedIn + } + + if (isLoggedIn == true) { + _sideEffects.send(SplashContract.SplashSideEffect.NavigateToHome) + } else { + _sideEffects.send(SplashContract.SplashSideEffect.NavigateToLogin) + } + } } } - fun clearUpdateState() { - _updateState.value = AppUpdateState.Latest + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(state: SplashContract.SplashState): SplashViewModel } + + companion object : + MavericksViewModelFactory by hiltMavericksViewModelFactory() } From 82d9b9697514d6bfc3280238ee54f3dda471c568 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:34:41 +0900 Subject: [PATCH 045/299] =?UTF-8?q?[REFACTOR/#252]=20FCM=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=A1=9C=EC=A7=81=20core=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EB=A1=9C=20=EB=B6=84=EB=A6=AC=ED=95=98=EC=97=AC=20Fcm?= =?UTF-8?q?TokenProvider=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/core/fcm/FcmTokenProvider.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/core/fcm/FcmTokenProvider.kt diff --git a/app/src/main/java/com/sopt/clody/core/fcm/FcmTokenProvider.kt b/app/src/main/java/com/sopt/clody/core/fcm/FcmTokenProvider.kt new file mode 100644 index 00000000..ae142941 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/fcm/FcmTokenProvider.kt @@ -0,0 +1,18 @@ +package com.sopt.clody.core.fcm + +import com.google.firebase.ktx.Firebase +import com.google.firebase.messaging.ktx.messaging +import kotlinx.coroutines.tasks.await +import timber.log.Timber +import javax.inject.Inject + +class FcmTokenProvider @Inject constructor() { + suspend fun getToken(): String? { + return try { + Firebase.messaging.token.await() + } catch (e: Exception) { + Timber.e("FCM 토큰 수신 실패: ${e.message}") + null + } + } +} From 97859a37a86feaa549becc3d74b1ebc260710fe3 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:38:23 +0900 Subject: [PATCH 046/299] =?UTF-8?q?[REFACTOR/#252]=20=EC=86=8C=EC=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20core:login?= =?UTF-8?q?=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20LoginSdk=20=EC=B6=94=EC=83=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KakaoLoginSdk 클래스를 별도로 분리해 login/logout/unlink 책임 위임 - ViewModel 내 직접 호출하던 카카오 로그인 로직 제거 - LoginSdk, LoginAccessToken 등 인터페이스 및 value class 도입 - DI 구조(LoginModule) 정리로 테스트와 확장성 개선 --- .../sopt/clody/core/login/KakaoAccessToken.kt | 4 + .../sopt/clody/core/login/KakaoLoginSdk.kt | 85 +++++++++++++++++++ .../sopt/clody/core/login/LoginAccessToken.kt | 5 ++ .../sopt/clody/core/login/LoginException.kt | 6 ++ .../com/sopt/clody/core/login/LoginModule.kt | 16 ++++ .../com/sopt/clody/core/login/LoginSdk.kt | 9 ++ 6 files changed, 125 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/core/login/KakaoAccessToken.kt create mode 100644 app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt create mode 100644 app/src/main/java/com/sopt/clody/core/login/LoginAccessToken.kt create mode 100644 app/src/main/java/com/sopt/clody/core/login/LoginException.kt create mode 100644 app/src/main/java/com/sopt/clody/core/login/LoginModule.kt create mode 100644 app/src/main/java/com/sopt/clody/core/login/LoginSdk.kt diff --git a/app/src/main/java/com/sopt/clody/core/login/KakaoAccessToken.kt b/app/src/main/java/com/sopt/clody/core/login/KakaoAccessToken.kt new file mode 100644 index 00000000..546a021e --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/login/KakaoAccessToken.kt @@ -0,0 +1,4 @@ +package com.sopt.clody.core.login + +@JvmInline +value class KakaoAccessToken(override val value: String) : LoginAccessToken diff --git a/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt b/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt new file mode 100644 index 00000000..216cf86c --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt @@ -0,0 +1,85 @@ +package com.sopt.clody.core.login + +import android.content.Context +import com.kakao.sdk.auth.model.OAuthToken +import com.kakao.sdk.common.model.AuthError +import com.kakao.sdk.common.model.ClientError +import com.kakao.sdk.common.model.ClientErrorCause +import com.kakao.sdk.user.UserApiClient +import kotlinx.coroutines.suspendCancellableCoroutine +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +@Singleton +class KakaoLoginSdk @Inject constructor() : LoginSdk { + override suspend fun login(context: Context): Result = runCatching { + suspendCancellableCoroutine { continuation -> + val callback: (OAuthToken?, Throwable?) -> Unit = callback@{ token, throwable -> + if (!continuation.isActive) { + return@callback + } + + when { + throwable != null -> { + if (throwable is ClientError && throwable.reason == ClientErrorCause.Cancelled) { + continuation.resumeWithException(LoginException.CancelException(throwable.message)) + return@callback + } + + continuation.resumeWithException(LoginException.AuthException(throwable.message)) + } + + token != null -> continuation.resume(KakaoAccessToken(token.accessToken)) + } + } + + val userApiClient = UserApiClient.instance + + if (userApiClient.isKakaoTalkLoginAvailable(context)) { + userApiClient.loginWithKakaoTalk( + context, + callback = callback@{ oAuthToken, throwable -> + // 카카오톡이 설치되어 있으나 로그인되어 있지 않은 경우 대응 + if (throwable is AuthError && throwable.statusCode == 302) { + userApiClient.loginWithKakaoAccount(context, callback = callback) + return@callback + } + + callback(oAuthToken, throwable) + }, + ) + } else { + // 카카오톡 웹 로그인 + userApiClient.loginWithKakaoAccount(context, callback = callback) + } + } + } + + override suspend fun logout(): Result = runCatching { + suspendCancellableCoroutine { continuation -> + val userApiClient = UserApiClient.instance + + userApiClient.logout { throwable -> + when { + throwable != null -> continuation.resumeWithException(LoginException.AuthException(throwable.message)) + else -> continuation.resume(Unit) + } + } + } + } + + override suspend fun unlink(): Result = runCatching { + suspendCancellableCoroutine { continuation -> + val userApiClient = UserApiClient.instance + + userApiClient.unlink { throwable -> + when { + throwable != null -> continuation.resumeWithException(LoginException.AuthException(throwable.message)) + else -> continuation.resume(Unit) + } + } + } + } +} diff --git a/app/src/main/java/com/sopt/clody/core/login/LoginAccessToken.kt b/app/src/main/java/com/sopt/clody/core/login/LoginAccessToken.kt new file mode 100644 index 00000000..b5f3caa6 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/login/LoginAccessToken.kt @@ -0,0 +1,5 @@ +package com.sopt.clody.core.login + +interface LoginAccessToken { + val value: String +} diff --git a/app/src/main/java/com/sopt/clody/core/login/LoginException.kt b/app/src/main/java/com/sopt/clody/core/login/LoginException.kt new file mode 100644 index 00000000..6c175e51 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/login/LoginException.kt @@ -0,0 +1,6 @@ +package com.sopt.clody.core.login + +sealed class LoginException(override val message: String?) : Exception(message) { + class CancelException(message: String?) : LoginException(message) + class AuthException(message: String?) : LoginException(message) +} diff --git a/app/src/main/java/com/sopt/clody/core/login/LoginModule.kt b/app/src/main/java/com/sopt/clody/core/login/LoginModule.kt new file mode 100644 index 00000000..9a9359d2 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/login/LoginModule.kt @@ -0,0 +1,16 @@ +package com.sopt.clody.core.login + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class LoginModule { + + @Binds + abstract fun bindLoginSdk( + kakaoLoginSdk: KakaoLoginSdk, + ): LoginSdk +} diff --git a/app/src/main/java/com/sopt/clody/core/login/LoginSdk.kt b/app/src/main/java/com/sopt/clody/core/login/LoginSdk.kt new file mode 100644 index 00000000..ddee77b8 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/login/LoginSdk.kt @@ -0,0 +1,9 @@ +package com.sopt.clody.core.login + +import android.content.Context + +interface LoginSdk { + suspend fun login(context: Context): Result + suspend fun logout(): Result + suspend fun unlink(): Result +} From 017caaa30e9f2b7cb69e8f0d166fb1f5627b191a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:41:02 +0900 Subject: [PATCH 047/299] =?UTF-8?q?[REFACTOR/#252]=20Login=EC=97=90=20Mave?= =?UTF-8?q?ricks=20Mvi=20=EA=B5=AC=EC=A1=B0=20=EB=B0=8F=20soc=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/login/LoginContract.kt | 23 ++++ .../ui/{auth => }/login/LoginScreen.kt | 66 ++++++++---- .../presentation/ui/login/LoginViewModel.kt | 101 ++++++++++++++++++ 3 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt rename app/src/main/java/com/sopt/clody/presentation/ui/{auth => }/login/LoginScreen.kt (64%) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt new file mode 100644 index 00000000..38b71267 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt @@ -0,0 +1,23 @@ +package com.sopt.clody.presentation.ui.login + +import android.content.Context +import com.airbnb.mvrx.MavericksState + +class LoginContract { + + data class LoginState( + val isLoading: Boolean = false, + val errorMessage: String? = null, + ) : MavericksState + + sealed class LoginIntent { + data class LoginWithKakao(val context: Context) : LoginIntent() + data object ClearError : LoginIntent() + } + + sealed interface LoginSideEffect { + data object NavigateToHome : LoginSideEffect + data object NavigateToSignUp : LoginSideEffect + data class ShowError(val message: String) : LoginSideEffect + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt similarity index 64% rename from app/src/main/java/com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt index edd916bd..b9a5d4ab 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/LoginScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.auth.login +package com.sopt.clody.presentation.ui.login import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -19,45 +18,64 @@ 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.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.airbnb.mvrx.compose.collectAsState +import com.airbnb.mvrx.compose.mavericksViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.sopt.clody.R import com.sopt.clody.presentation.ui.auth.component.button.KaKaoButton -import com.sopt.clody.presentation.ui.auth.signup.SignUpViewModel import com.sopt.clody.presentation.ui.component.LoadingScreen -import com.sopt.clody.presentation.utils.base.UiState +import com.sopt.clody.presentation.ui.component.dialog.FailureDialog +import com.sopt.clody.presentation.utils.base.BasePreview +import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage +import com.sopt.clody.presentation.utils.extension.repeatOnStarted import com.sopt.clody.ui.theme.ClodyTheme @Composable fun LoginRoute( - navigateToTermsOfService: () -> Unit, + navigateToSignUp: () -> Unit, navigateToHome: () -> Unit, + viewModel: LoginViewModel = mavericksViewModel(), ) { - val viewModel: SignUpViewModel = hiltViewModel() - val signInState by viewModel.signInState.collectAsState() + val state by viewModel.collectAsState() val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current - LaunchedEffect(signInState.uiState) { - when (signInState.uiState) { - is UiState.Success -> navigateToHome() - is UiState.Failure -> navigateToTermsOfService() - else -> Unit + LaunchedEffect(viewModel) { + lifecycleOwner.repeatOnStarted { + viewModel.sideEffects.collect { sideEffect -> + when (sideEffect) { + is LoginContract.LoginSideEffect.NavigateToHome -> navigateToHome() + is LoginContract.LoginSideEffect.NavigateToSignUp -> navigateToSignUp() + is LoginContract.LoginSideEffect.ShowError -> { + // 삐용삐용 여기 뭘로 처리할까? + } + } + } } } LoginScreen( - isLoading = signInState.uiState is UiState.Loading, - onSignInClick = { viewModel.signInWithKakao(context) }, + isLoading = state.isLoading, + onLoginClick = { + viewModel.postIntent(LoginContract.LoginIntent.LoginWithKakao(context)) + }, ) + + // 에러 메시지 추가로 다이얼로그로 처리하고 싶다면? + state.errorMessage?.let { message -> + FailureDialog(message = message) { + viewModel.postIntent(LoginContract.LoginIntent.ClearError) + } + } } @Composable fun LoginScreen( isLoading: Boolean, - onSignInClick: () -> Unit, + onLoginClick: () -> Unit, ) { val systemUiController = rememberSystemUiController() val backgroundColor = ClodyTheme.colors.white @@ -73,7 +91,7 @@ fun LoginScreen( bottomBar = { KaKaoButton( text = stringResource(id = R.string.signup_btn_kakao), - onClick = onSignInClick, + onClick = onLoginClick, modifier = Modifier .fillMaxWidth() .navigationBarsPadding() @@ -113,11 +131,13 @@ fun LoginScreen( } } -@Preview(showBackground = true) +@ClodyPreview @Composable fun LoginScreenPreview() { - LoginScreen( - isLoading = false, - onSignInClick = {}, - ) + BasePreview { + LoginScreen( + isLoading = false, + onLoginClick = {}, + ) + } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt new file mode 100644 index 00000000..3304a338 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -0,0 +1,101 @@ +package com.sopt.clody.presentation.ui.login + +import android.content.Context +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.hilt.AssistedViewModelFactory +import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory +import com.sopt.clody.core.fcm.FcmTokenProvider +import com.sopt.clody.core.login.LoginSdk +import com.sopt.clody.data.remote.dto.request.LoginRequestDto +import com.sopt.clody.domain.repository.AuthRepository +import com.sopt.clody.domain.repository.TokenRepository +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +class LoginViewModel @AssistedInject constructor( + @Assisted initialState: LoginContract.LoginState, + private val loginSdk: LoginSdk, + private val authRepository: AuthRepository, + private val tokenRepository: TokenRepository, + private val fcmTokenProvider: FcmTokenProvider, +) : MavericksViewModel(initialState) { + + private val _intents = Channel(BUFFERED) + private val _sideEffects = Channel(BUFFERED) + val sideEffects = _sideEffects.receiveAsFlow() + + init { + _intents + .receiveAsFlow() + .onEach(::handleIntent) + .launchIn(viewModelScope) + } + + fun postIntent(intent: LoginContract.LoginIntent) { + viewModelScope.launch { _intents.send(intent) } + } + + private suspend fun handleIntent(intent: LoginContract.LoginIntent) { + when (intent) { + is LoginContract.LoginIntent.LoginWithKakao -> loginWithKakao(intent.context) + LoginContract.LoginIntent.ClearError -> setState { copy(errorMessage = null) } + } + } + + private suspend fun loginWithKakao(context: Context) { + setState { copy(isLoading = true, errorMessage = null) } + + loginSdk.login(context).fold( + onSuccess = { accessToken -> + validateUser("Bearer ${accessToken.value}") + }, + onFailure = { error -> + setState { copy(isLoading = false) } + _sideEffects.send( + LoginContract.LoginSideEffect.ShowError( + error.message ?: "로그인에 실패했습니다.", + ), + ) + }, + ) + } + + private suspend fun validateUser(token: String) { + val fcmToken = fcmTokenProvider.getToken().orEmpty() + val request = LoginRequestDto(platform = "kakao", fcmToken = fcmToken) + + authRepository.signIn(token, request).fold( + onSuccess = { + tokenRepository.setTokens(it.accessToken, it.refreshToken) + setState { copy(isLoading = false) } + _sideEffects.send(LoginContract.LoginSideEffect.NavigateToHome) + }, + onFailure = { error -> + val message = error.message.orEmpty() + setState { copy(isLoading = false) } + + if (message.contains("404") || message.contains("유저가 없습니다")) { + _sideEffects.send(LoginContract.LoginSideEffect.NavigateToSignUp) + } else { + _sideEffects.send(LoginContract.LoginSideEffect.ShowError("로그인 실패")) + } + }, + ) + } + + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(state: LoginContract.LoginState): LoginViewModel + } + + companion object : + MavericksViewModelFactory by hiltMavericksViewModelFactory() +} From 4ee4230a30e825ee9c834bf50175a6536b68843b Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:51:40 +0900 Subject: [PATCH 048/299] =?UTF-8?q?[REFACTOR/#252]=20SignUp(=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85)=20=ED=9D=90=EB=A6=84=20Route=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=ED=99=94=20=EB=B0=8F=20MVI=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TermsOfService, Nickname screen 분리 구조를 SignUpRoute 단일화 구조로 통합 - Mavericks 기반 MVI 패턴으로 SignUp 상태 및 이벤트 처리 구조 전환 - fcmTokenProvider, LoginSdk 등 SOC 기반 의존성 분리 적용 - back navigation 대응을 위한 navigateToPrevious 전달 구조 반영 --- .../ui/auth/signup/SignUpContract.kt | 46 +++ .../ui/auth/signup/SignUpScreen.kt | 88 ++++++ .../ui/auth/signup/SignUpViewModel.kt | 297 ++++++++---------- .../signup/navigation/SignUpNavigation.kt | 35 +-- .../ui/auth/signup/page/NicknamePage.kt | 157 +++++++++ .../ui/auth/signup/page/TermsOfServicePage.kt | 176 +++++++++++ 6 files changed, 608 insertions(+), 191 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt new file mode 100644 index 00000000..22334a9b --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt @@ -0,0 +1,46 @@ +package com.sopt.clody.presentation.ui.auth.signup + +import android.content.Context +import com.airbnb.mvrx.MavericksState + +class SignUpContract { + data class SignUpState( + val currentStep: Step = Step.TERMS, + val nickname: String = "", + val isNicknameFocused: Boolean = false, + val isValidNickname: Boolean = true, + val nicknameMessage: String = DEFAULT_NICKNAME_MESSAGE, + val isLoading: Boolean = false, + val errorMessage: String? = null, + val serviceChecked: Boolean = false, + val privacyChecked: Boolean = false, + val allChecked: Boolean = false, + ) : MavericksState { + enum class Step { + TERMS, NICKNAME + } + } + + sealed class SignUpIntent { + data class SetNickname(val value: String) : SignUpIntent() + data class SetNicknameFocus(val isFocused: Boolean) : SignUpIntent() + data object ProceedTerms : SignUpIntent() + data class CompleteSignUp(val context: Context) : SignUpIntent() + data object ClearError : SignUpIntent() + + data class ToggleAllChecked(val checked: Boolean) : SignUpIntent() + data class ToggleServiceChecked(val checked: Boolean) : SignUpIntent() + data class TogglePrivacyChecked(val checked: Boolean) : SignUpIntent() + + data object BackToTerms : SignUpIntent() + } + + sealed interface SignUpSideEffect { + data object NavigateToTimeReminder : SignUpSideEffect + data class ShowMessage(val message: String) : SignUpSideEffect + } + + companion object { + const val DEFAULT_NICKNAME_MESSAGE = "특수문자, 띄어쓰기 없이 작성해주세요" + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt new file mode 100644 index 00000000..c249b882 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt @@ -0,0 +1,88 @@ +package com.sopt.clody.presentation.ui.auth.signup + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.airbnb.mvrx.compose.collectAsState +import com.airbnb.mvrx.compose.mavericksViewModel +import com.sopt.clody.presentation.ui.auth.signup.page.NickNamePage +import com.sopt.clody.presentation.ui.auth.signup.page.TermsOfServicePage +import com.sopt.clody.presentation.ui.component.dialog.FailureDialog +import com.sopt.clody.presentation.utils.extension.repeatOnStarted + +@Composable +fun SignUpRoute( + viewModel: SignUpViewModel = mavericksViewModel(), + navigateToHome: () -> Unit, + navigateToPrevious: () -> Unit, +) { + val state by viewModel.collectAsState() + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(viewModel) { + lifecycleOwner.repeatOnStarted { + viewModel.sideEffects.collect { effect -> + when (effect) { + is SignUpContract.SignUpSideEffect.NavigateToTimeReminder -> navigateToHome() + is SignUpContract.SignUpSideEffect.ShowMessage -> { + // 삐용삐용 에러 대응을 어떻게 할까요? + } + } + } + } + } + + SignUpScreen( + state = state, + onIntent = { viewModel.postIntent(it) }, + context = context, + navigateToPrevious = navigateToPrevious, + ) + + state.errorMessage?.let { + FailureDialog(message = it) { + viewModel.postIntent(SignUpContract.SignUpIntent.ClearError) + } + } +} + +@Composable +fun SignUpScreen( + state: SignUpContract.SignUpState, + onIntent: (SignUpContract.SignUpIntent) -> Unit, + context: Context, + navigateToPrevious: () -> Unit, +) { + when (state.currentStep) { + SignUpContract.SignUpState.Step.TERMS -> { + TermsOfServicePage( + allChecked = state.allChecked, + serviceChecked = state.serviceChecked, + privacyChecked = state.privacyChecked, + onToggleAll = { onIntent(SignUpContract.SignUpIntent.ToggleAllChecked(it)) }, + onToggleService = { onIntent(SignUpContract.SignUpIntent.ToggleServiceChecked(it)) }, + onTogglePrivacy = { onIntent(SignUpContract.SignUpIntent.TogglePrivacyChecked(it)) }, + onAgreeClick = { onIntent(SignUpContract.SignUpIntent.ProceedTerms) }, + navigateToPrevious = navigateToPrevious, + ) + } + + SignUpContract.SignUpState.Step.NICKNAME -> { + NickNamePage( + nickname = state.nickname, + onNicknameChange = { onIntent(SignUpContract.SignUpIntent.SetNickname(it)) }, + onCompleteClick = { onIntent(SignUpContract.SignUpIntent.CompleteSignUp(context)) }, + onBackClick = { onIntent(SignUpContract.SignUpIntent.BackToTerms) }, + isLoading = state.isLoading, + isValidNickname = state.isValidNickname, + nicknameMessage = state.nicknameMessage, + isFocused = state.isNicknameFocused, + onFocusChanged = { onIntent(SignUpContract.SignUpIntent.SetNicknameFocus(it)) }, + ) + } + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index 11d3cfe6..d3a9acb8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -1,209 +1,178 @@ package com.sopt.clody.presentation.ui.auth.signup import android.content.Context -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.kakao.sdk.auth.model.OAuthToken -import com.kakao.sdk.user.UserApiClient -import com.sopt.clody.ClodyFirebaseMessagingService -import com.sopt.clody.data.remote.dto.request.LoginRequestDto +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.hilt.AssistedViewModelFactory +import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory +import com.airbnb.mvrx.withState +import com.sopt.clody.core.fcm.FcmTokenProvider +import com.sopt.clody.core.login.LoginSdk import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository -import com.sopt.clody.presentation.ui.auth.login.SignInState -import com.sopt.clody.presentation.utils.base.UiState -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce +import com.sopt.clody.presentation.ui.auth.signup.SignUpContract.Companion.DEFAULT_NICKNAME_MESSAGE +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -@OptIn(FlowPreview::class) -@HiltViewModel -class SignUpViewModel @Inject constructor( + +class SignUpViewModel @AssistedInject constructor( + @Assisted initialState: SignUpContract.SignUpState, + private val loginSdk: LoginSdk, private val authRepository: AuthRepository, private val tokenRepository: TokenRepository, + private val fcmTokenProvider: FcmTokenProvider, private val networkUtil: NetworkUtil, -) : ViewModel() { +) : MavericksViewModel(initialState) { - private val _signInState = MutableStateFlow(SignInState()) - val signInState: StateFlow = _signInState + private val _intents = Channel(BUFFERED) + private val _sideEffects = Channel(BUFFERED) + val sideEffects = _sideEffects.receiveAsFlow() - private val _signUpState = MutableStateFlow(SignUpState()) - val signUpState: StateFlow = _signUpState + init { + _intents + .receiveAsFlow() + .onEach(::handleIntent) + .launchIn(viewModelScope) + } - private var accessToken: String? = null + fun postIntent(intent: SignUpContract.SignUpIntent) { + viewModelScope.launch { _intents.send(intent) } + } - private val _nickname = MutableStateFlow("") - val nickname: StateFlow = _nickname + private suspend fun handleIntent(intent: SignUpContract.SignUpIntent) { + when (intent) { + is SignUpContract.SignUpIntent.SetNickname -> { + val isValid = validateNickname(intent.value) + setState { + copy( + nickname = intent.value, + isValidNickname = isValid, + nicknameMessage = if (intent.value.isEmpty()) { + DEFAULT_NICKNAME_MESSAGE + } else if (isValid) { + DEFAULT_NICKNAME_MESSAGE + } else { + "사용할 수 없는 닉네임이에요" + }, + ) + } + } - private val _isValidNickname = MutableStateFlow(true) - val isValidNickname: StateFlow = _isValidNickname + is SignUpContract.SignUpIntent.SetNicknameFocus -> { + setState { copy(isNicknameFocused = intent.isFocused) } + } - private val _nicknameMessage = MutableStateFlow(DEFAULT_NICKNAME_MESSAGE) - val nicknameMessage: StateFlow = _nicknameMessage + SignUpContract.SignUpIntent.ProceedTerms -> { + setState { copy(currentStep = SignUpContract.SignUpState.Step.NICKNAME) } + } - init { - debounceNicknameValidation() - } + is SignUpContract.SignUpIntent.CompleteSignUp -> { + signUp(intent.context) + } - fun signInWithKakao(context: Context) { - viewModelScope.launch { - _signInState.value = SignInState(UiState.Loading) - val tokenResult = runCatching { loginWithKakao(context) } - tokenResult.onSuccess { token -> - accessToken = token.accessToken - fetchKakaoUserInfo(context) - }.onFailure { - _signInState.value = SignInState(UiState.Failure(it.localizedMessage ?: UNKNOWN_ERROR)) + SignUpContract.SignUpIntent.ProceedTerms -> { + setState { copy(currentStep = SignUpContract.SignUpState.Step.NICKNAME) } } - } - } - fun proceedWithSignUp(context: Context) { - viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _signUpState.value = SignUpState(UiState.Failure(FAILURE_NETWORK_MESSAGE)) - return@launch + SignUpContract.SignUpIntent.ClearError -> { + setState { copy(errorMessage = null) } } - _signUpState.value = SignUpState(UiState.Loading) - val tokenResult = runCatching { loginWithKakao(context) } - tokenResult.onSuccess { token -> - accessToken = token.accessToken - performSignUp(context) - }.onFailure { - _signUpState.value = SignUpState(UiState.Failure(it.localizedMessage ?: UNKNOWN_ERROR)) + + is SignUpContract.SignUpIntent.ToggleAllChecked -> { + setState { + copy( + allChecked = intent.checked, + serviceChecked = intent.checked, + privacyChecked = intent.checked, + ) + } } - } - } - private suspend fun loginWithKakao(context: Context): OAuthToken { - return suspendCancellableCoroutine { continuation -> - val callback: (OAuthToken?, Throwable?) -> Unit = { token, error -> - if (error != null) { - continuation.resumeWithException(error) - } else if (token != null) { - continuation.resume(token) + is SignUpContract.SignUpIntent.ToggleServiceChecked -> { + setState { + val all = intent.checked && this.privacyChecked + copy( + serviceChecked = intent.checked, + allChecked = all, + ) } } - if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { - UserApiClient.instance.loginWithKakaoTalk(context, callback = callback) - } else { - UserApiClient.instance.loginWithKakaoAccount(context, callback = callback) + + is SignUpContract.SignUpIntent.TogglePrivacyChecked -> { + setState { + val all = this.serviceChecked && intent.checked + copy( + privacyChecked = intent.checked, + allChecked = all, + ) + } } - } - } - private fun fetchKakaoUserInfo(context: Context) { - UserApiClient.instance.me { user, error -> - if (error != null) { - _signInState.value = SignInState(UiState.Failure(error.localizedMessage)) - } else if (user != null) { - val fcmToken = ClodyFirebaseMessagingService.getTokenFromPreferences(context) ?: "" - val requestSignInDto = LoginRequestDto(platform = KAKAO_PLATFORM, fcmToken = fcmToken) - validateUser("Bearer ${accessToken.orEmpty()}", requestSignInDto) + SignUpContract.SignUpIntent.BackToTerms -> { + setState { + copy( + currentStep = SignUpContract.SignUpState.Step.TERMS, + nickname = "", + isNicknameFocused = false, + isValidNickname = true, + nicknameMessage = SignUpContract.DEFAULT_NICKNAME_MESSAGE, + ) + } } } } - private fun validateUser(authorization: String, requestSignInDto: LoginRequestDto) { + fun signUp(context: Context) { viewModelScope.launch { - authRepository.signIn(authorization, requestSignInDto).fold( - onSuccess = { response -> - storeTokens(response.accessToken, response.refreshToken) - _signInState.value = SignInState(UiState.Success(USER_EXISTS)) - }, - onFailure = { - val message = it.localizedMessage ?: UNKNOWN_ERROR - val uiState = if (message.contains("404")) { - UiState.Failure(USER_NOT_FOUND_ERROR) - } else { - UiState.Failure(message) + val state = withState(this@SignUpViewModel) { it } + if (!networkUtil.isNetworkAvailable()) { + setState { copy(errorMessage = "네트워크 연결을 확인해주세요.") } + return@launch + } + setState { copy(isLoading = true) } + loginSdk.login(context).fold( + onSuccess = { accessToken -> + launch { + val fcm = fcmTokenProvider.getToken().orEmpty() + val req = SignUpRequestDto("kakao", state.nickname, fcm) + authRepository.signUp("Bearer ${accessToken.value}", req).fold( + onSuccess = { + tokenRepository.setTokens(it.accessToken, it.refreshToken) + _sideEffects.send(SignUpContract.SignUpSideEffect.NavigateToTimeReminder) + }, + onFailure = { + setState { copy(errorMessage = it.message ?: "알 수 없는 오류") } + }, + ) + + setState { copy(isLoading = false) } } - _signInState.value = SignInState(uiState) - }, - ) - } - } - - private fun storeTokens(accessToken: String, refreshToken: String) { - viewModelScope.launch { - tokenRepository.setTokens(accessToken, refreshToken) - } - } - - fun setNickname(nickname: String) { - _nickname.value = nickname - } - - private fun performSignUp(context: Context) { - val authorization = "Bearer ${accessToken.orEmpty()}" - val fcmToken = ClodyFirebaseMessagingService.getTokenFromPreferences(context) ?: "" - viewModelScope.launch { - authRepository.signUp( - authorization, - SignUpRequestDto(platform = KAKAO_PLATFORM, name = nickname.value, fcmToken = fcmToken), - ).fold( - onSuccess = { response -> - _signUpState.value = SignUpState(UiState.Success(SIGN_UP_SUCCESS)) - storeTokens(response.accessToken, response.refreshToken) }, - onFailure = { error -> - val errorMessage = if (error.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE - } else { - error.localizedMessage ?: UNKNOWN_ERROR - } - _signUpState.value = SignUpState(UiState.Failure(errorMessage)) + onFailure = { + setState { copy(errorMessage = it.message ?: "로그인 실패", isLoading = false) } }, ) } } - private fun debounceNicknameValidation() { - viewModelScope.launch { - _nickname - .debounce(NICKNAME_VALIDATION_DELAY) - .collectLatest { nickname -> - validateNickname(nickname) - } - } - } - - private fun validateNickname(nickname: String) { - if (nickname.isNotEmpty()) { - val isValid = nickname.matches(Regex(NICKNAME_PATTERN)) - _isValidNickname.value = isValid - _nicknameMessage.value = if (isValid) DEFAULT_NICKNAME_MESSAGE else FAILURE_NICKNAME_MESSAGE - } else { - _isValidNickname.value = true - _nicknameMessage.value = DEFAULT_NICKNAME_MESSAGE - } + private fun validateNickname(nickname: String): Boolean { + val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,10}$".toRegex() + return nickname.matches(regex) } - fun resetSignUpState() { - _signUpState.value = SignUpState() + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(state: SignUpContract.SignUpState): SignUpViewModel } - companion object { - private const val USER_EXISTS = "유저가 이미 존재합니다" - private const val SIGN_UP_SUCCESS = "회원가입 성공" - private const val USER_NOT_FOUND_ERROR = "유저를 찾을 수 없습니다" - private const val KAKAO_PLATFORM = "kakao" - - private const val NICKNAME_VALIDATION_DELAY = 300L - private const val NICKNAME_PATTERN = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,10}$" - private const val DEFAULT_NICKNAME_MESSAGE = "특수문자, 띄어쓰기 없이 작성해주세요" - private const val FAILURE_NICKNAME_MESSAGE = "사용할 수 없는 닉네임이에요" - } + companion object : + MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt index 35b29501..5c921424 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt @@ -4,42 +4,23 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable -import com.sopt.clody.presentation.ui.auth.signup.NicknameRoute -import com.sopt.clody.presentation.ui.auth.signup.TermsOfServiceRoute +import com.sopt.clody.presentation.ui.auth.signup.SignUpRoute import com.sopt.clody.presentation.utils.navigation.Route -fun NavGraphBuilder.termsOfServiceScreen( - navigateToNickname: () -> Unit, - navigateToLogin: () -> Unit, -) { - composable { - TermsOfServiceRoute( - navigateToNickname = navigateToNickname, - navigateToLogin = navigateToLogin, - ) - } -} - -fun NavGraphBuilder.nicknameScreen( - navigateToReminder: () -> Unit, +fun NavGraphBuilder.signUpScreen( + navigateToHome: () -> Unit, navigateToPrevious: () -> Unit, ) { - composable { - NicknameRoute( - navigateToReminder = navigateToReminder, + composable { + SignUpRoute( + navigateToHome = navigateToHome, navigateToPrevious = navigateToPrevious, ) } } -fun NavController.navigateToNickname( - navOptions: NavOptionsBuilder.() -> Unit = {}, -) { - navigate(Route.Nickname, navOptions) -} - -fun NavController.navigateToTermsOfService( +fun NavController.navigateToSignUp( navOptions: NavOptionsBuilder.() -> Unit = {}, ) { - navigate(Route.TermsOfService, navOptions) + navigate(Route.SignUp, navOptions) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt new file mode 100644 index 00000000..65e3ebc7 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt @@ -0,0 +1,157 @@ +package com.sopt.clody.presentation.ui.auth.signup.page + +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.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.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import com.sopt.clody.R +import com.sopt.clody.presentation.ui.auth.component.textfield.NickNameTextField +import com.sopt.clody.presentation.ui.component.LoadingScreen +import com.sopt.clody.presentation.ui.component.button.ClodyButton +import com.sopt.clody.presentation.utils.base.BasePreview +import com.sopt.clody.presentation.utils.base.ClodyPreview +import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage +import com.sopt.clody.ui.theme.ClodyTheme + +@Composable +fun NickNamePage( + nickname: String, + isValidNickname: Boolean, + nicknameMessage: String, + isLoading: Boolean, + isFocused: Boolean, + onNicknameChange: (String) -> Unit, + onFocusChanged: (Boolean) -> Unit, + onCompleteClick: () -> Unit, + onBackClick: () -> Unit, +) { + val focusManager = LocalFocusManager.current + + val annotatedString = buildAnnotatedString { + withStyle(style = SpanStyle(color = ClodyTheme.colors.gray04)) { + append("${nickname.length}") + } + withStyle(style = SpanStyle(color = ClodyTheme.colors.gray06)) { + append(" / ") + } + withStyle(style = SpanStyle(color = ClodyTheme.colors.gray06)) { + append("10") + } + } + + Scaffold( + topBar = { + IconButton( + onClick = onBackClick, + modifier = Modifier + .statusBarsPadding() + .padding(start = 8.dp), + ) { + Image( + painter = painterResource(id = R.drawable.ic_nickname_back), + contentDescription = null, + ) + } + }, + bottomBar = { + ClodyButton( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .imePadding() + .padding(horizontal = 24.dp) + .padding(bottom = 28.dp), + onClick = { + focusManager.clearFocus() + onCompleteClick() + }, + text = "다음", + enabled = nickname.isNotEmpty() && isValidNickname, + ) + }, + content = { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(ClodyTheme.colors.white) + .padding(paddingValues) + .padding(horizontal = 24.dp), + ) { + Spacer(modifier = Modifier.heightForScreenPercentage(0.056f)) + Text("닉네임 입력", style = ClodyTheme.typography.head1) + Spacer(modifier = Modifier.heightForScreenPercentage(0.06f)) + NickNameTextField( + value = nickname, + onValueChange = onNicknameChange, + hint = "닉네임을 입력하세요", + isFocused = isFocused, + isValid = isValidNickname, + onFocusChanged = onFocusChanged, + onRemove = { onNicknameChange("") }, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = nicknameMessage, + color = when { + nickname.isEmpty() -> ClodyTheme.colors.gray04 + isValidNickname -> ClodyTheme.colors.gray04 + else -> ClodyTheme.colors.red + }, + style = ClodyTheme.typography.detail1Regular, + ) + Text( + text = annotatedString, + style = ClodyTheme.typography.detail1Medium, + ) + } + } + }, + ) + + if (isLoading) { + LoadingScreen() + } +} + +@ClodyPreview +@Composable +private fun NicknamePagePreview() { + BasePreview { + NickNamePage( + nickname = "클로디", + isValidNickname = true, + nicknameMessage = "사용 가능한 닉네임입니다.", + isLoading = false, + isFocused = false, + onNicknameChange = {}, + onFocusChanged = {}, + onCompleteClick = {}, + onBackClick = {}, + ) + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt new file mode 100644 index 00000000..31d46fcd --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt @@ -0,0 +1,176 @@ +package com.sopt.clody.presentation.ui.auth.signup.page + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.sopt.clody.R +import com.sopt.clody.presentation.ui.auth.component.checkbox.CustomCheckbox +import com.sopt.clody.presentation.ui.component.button.ClodyButton +import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider +import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls +import com.sopt.clody.presentation.ui.setting.screen.onClickSettingOption +import com.sopt.clody.presentation.utils.base.BasePreview +import com.sopt.clody.presentation.utils.base.ClodyPreview +import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage +import com.sopt.clody.ui.theme.ClodyTheme + +@Composable +fun TermsOfServicePage( + allChecked: Boolean, + serviceChecked: Boolean, + privacyChecked: Boolean, + onToggleAll: (Boolean) -> Unit, + onToggleService: (Boolean) -> Unit, + onTogglePrivacy: (Boolean) -> Unit, + onAgreeClick: () -> Unit, + navigateToPrevious: () -> Unit, +) { + val context = LocalContext.current + val isAgreeButtonEnabled = serviceChecked && privacyChecked + + Scaffold( + topBar = { + IconButton( + onClick = navigateToPrevious, + modifier = Modifier + .statusBarsPadding() + .padding(start = 8.dp), + ) { + Image( + painter = painterResource(id = R.drawable.ic_nickname_back), + contentDescription = null, + ) + } + }, + bottomBar = { + ClodyButton( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(horizontal = 24.dp) + .padding(bottom = 28.dp), + onClick = onAgreeClick, + text = stringResource(R.string.terms_next), + enabled = isAgreeButtonEnabled, + ) + }, + content = { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(color = ClodyTheme.colors.white) + .padding(paddingValues) + .padding(horizontal = 24.dp), + ) { + Spacer(modifier = Modifier.heightForScreenPercentage(0.056f)) + Text( + text = stringResource(R.string.terms_title), + color = ClodyTheme.colors.gray01, + style = ClodyTheme.typography.head1, + ) + Spacer(modifier = Modifier.heightForScreenPercentage(0.06f)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.terms_agree_all), + modifier = Modifier.weight(1f), + color = ClodyTheme.colors.gray01, + style = ClodyTheme.typography.head3, + ) + CustomCheckbox( + checked = allChecked, + onCheckedChange = onToggleAll, + size = 25.dp, + checkedImageRes = R.drawable.ic_terms_check_on_25, + uncheckedImageRes = R.drawable.ic_terms_check_off_25, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider(color = ClodyTheme.colors.gray07, thickness = 1.dp) + Spacer(modifier = Modifier.height(16.dp)) + TermsCheckboxRow( + text = stringResource(R.string.terms_service_use), + checked = serviceChecked, + onCheckedChange = onToggleService, + onClickMore = { onClickSettingOption(context, SettingOptionUrls.TERMS_OF_SERVICE_URL) }, + ) + Spacer(modifier = Modifier.height(8.dp)) + TermsCheckboxRow( + text = stringResource(R.string.terms_service_privacy), + checked = privacyChecked, + onCheckedChange = onTogglePrivacy, + onClickMore = { onClickSettingOption(context, SettingOptionUrls.PRIVACY_POLICY_URL) }, + ) + } + }, + ) +} + +@Composable +fun TermsCheckboxRow( + text: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + onClickMore: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text, style = ClodyTheme.typography.body1Medium) + Spacer(modifier = Modifier.width(8.dp)) + Image( + painter = painterResource(id = R.drawable.ic_terms_next), + contentDescription = null, + modifier = Modifier + .clickable(onClick = onClickMore), + ) + Spacer(modifier = Modifier.weight(1f)) + CustomCheckbox( + checked = checked, + onCheckedChange = onCheckedChange, + size = 23.dp, + checkedImageRes = R.drawable.ic_terms_check_on_23, + uncheckedImageRes = R.drawable.ic_terms_check_off_23, + ) + } +} + +@ClodyPreview +@Composable +private fun TermsOfServicePagePreview() { + BasePreview { + TermsOfServicePage( + allChecked = false, + serviceChecked = false, + privacyChecked = false, + onToggleAll = {}, + onToggleService = {}, + onTogglePrivacy = {}, + onAgreeClick = {}, + navigateToPrevious = {}, + ) + } +} From 68e9f1a201a73d48138406ca6927b748b73cba26 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:52:23 +0900 Subject: [PATCH 049/299] =?UTF-8?q?[REFACTOR/#252]=20SignUp=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?navigation=20parameter=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/{auth => }/login/navigation/LoginNavigation.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename app/src/main/java/com/sopt/clody/presentation/ui/{auth => }/login/navigation/LoginNavigation.kt (70%) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/navigation/LoginNavigation.kt similarity index 70% rename from app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/login/navigation/LoginNavigation.kt index 2eff62a1..a8d4a1a7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/navigation/LoginNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/navigation/LoginNavigation.kt @@ -1,19 +1,19 @@ -package com.sopt.clody.presentation.ui.auth.login.navigation +package com.sopt.clody.presentation.ui.login.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable -import com.sopt.clody.presentation.ui.auth.login.LoginRoute +import com.sopt.clody.presentation.ui.login.LoginRoute import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.loginScreen( - navigateToTermsOfService: () -> Unit, + navigateToSignUp: () -> Unit, navigateToHome: () -> Unit, ) { composable { LoginRoute( - navigateToTermsOfService = navigateToTermsOfService, + navigateToSignUp = navigateToSignUp, navigateToHome = navigateToHome, ) } From 0fdadc0207dd58e6abdf31b99e3368e07493dfc6 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:54:14 +0900 Subject: [PATCH 050/299] =?UTF-8?q?[REFACTOR/#252]=20NickNameTextField=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EA=B4=80=EB=A6=AC=EB=A5=BC=20TextFieldValue=20?= =?UTF-8?q?=E2=86=92=20String=20=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 TextFieldValue 기반 로직을 단순한 String 기반으로 리팩토링 - 내부 상태를 ViewModel에서 통합 관리할 수 있도록 구조 정비 --- .../component/textfield/NickNameTextField.kt | 124 ++++++++---------- 1 file changed, 54 insertions(+), 70 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/textfield/NickNameTextField.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/textfield/NickNameTextField.kt index 906a87bf..a688900f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/textfield/NickNameTextField.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/textfield/NickNameTextField.kt @@ -3,8 +3,6 @@ package com.sopt.clody.presentation.ui.auth.component.textfield import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,25 +10,25 @@ 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.text.BasicTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.ui.theme.ClodyTheme + @Composable fun NickNameTextField( - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, + value: String, + onValueChange: (String) -> Unit, isFocused: Boolean, isValid: Boolean, onRemove: () -> Unit, @@ -40,83 +38,69 @@ fun NickNameTextField( ) { val maxLength = 10 - Box(modifier = modifier) { - BasicTextField( - value = value, - onValueChange = { - if (it.text.length <= maxLength) { - onValueChange(it) - } + BasicTextField( + value = value, + onValueChange = { + if (it.length <= maxLength) { + onValueChange(it) + } + }, + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .onFocusChanged { focusState -> + onFocusChanged(focusState.isFocused) }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .onFocusChanged { focusState -> - onFocusChanged(focusState.isFocused) - }, - singleLine = true, - textStyle = TextStyle(color = ClodyTheme.colors.gray01), - cursorBrush = SolidColor(ClodyTheme.colors.gray01), - decorationBox = { innerTextField -> - Column( - Modifier - .fillMaxWidth() - .padding(horizontal = 0.dp, vertical = 0.dp), - verticalArrangement = Arrangement.Center, + singleLine = true, + textStyle = TextStyle(color = ClodyTheme.colors.gray01), + cursorBrush = SolidColor(ClodyTheme.colors.gray01), + decorationBox = { innerTextField -> + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Box(modifier = Modifier.weight(1f)) { - if (value.text.isEmpty() && !isFocused) { - Text( - text = hint, - style = ClodyTheme.typography.body1Medium, - color = ClodyTheme.colors.gray05, - ) - } - innerTextField() // 실제 입력 필드 - } - Box( - modifier = Modifier - .clickable( - onClick = onRemove, - indication = null, - interactionSource = remember { MutableInteractionSource() }, - ), - contentAlignment = Alignment.Center, - ) { - Image( - painter = painterResource(id = R.drawable.ic_nickname_delete), - contentDescription = null, + Box(modifier = Modifier.weight(1f)) { + if (value.isEmpty() && !isFocused) { + Text( + text = hint, + style = ClodyTheme.typography.body1Medium, + color = ClodyTheme.colors.gray05, ) } + innerTextField() } - Spacer(modifier = Modifier.height(4.dp)) - Box( - modifier = Modifier - .height(2.dp) - .fillMaxWidth() - .background( - when { - isValid.not() -> ClodyTheme.colors.red - isFocused -> ClodyTheme.colors.mainYellow - else -> ClodyTheme.colors.gray08 - }, - ), + Spacer(modifier = Modifier.width(8.dp)) + Image( + painter = painterResource(id = R.drawable.ic_nickname_delete), + contentDescription = null, + modifier = Modifier.clickable { onRemove() }, ) } - }, - ) - } + Spacer(modifier = Modifier.height(4.dp)) + Box( + modifier = Modifier + .height(2.dp) + .fillMaxWidth() + .background( + when { + value.isEmpty() -> ClodyTheme.colors.gray08 + !isValid -> ClodyTheme.colors.red + isFocused -> ClodyTheme.colors.mainYellow + else -> ClodyTheme.colors.gray08 + }, + ), + ) + } + }, + ) } @Preview(showBackground = true) @Composable fun PreviewNickNameTextField() { NickNameTextField( - value = TextFieldValue(""), + value = "닉네임", onValueChange = {}, isFocused = false, isValid = true, From 0237d578c38f2d4c02c977cfac8872d39e7fe73f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:55:43 +0900 Subject: [PATCH 051/299] =?UTF-8?q?[REFACTOR/#252]=20SignUp=20=ED=9D=90?= =?UTF-8?q?=EB=A6=84=20=ED=86=B5=ED=95=A9=20=EB=B0=8F=20parameter=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 --- .../presentation/ui/main/ClodyNavHost.kt | 20 +++++++------------ .../presentation/utils/navigation/Route.kt | 5 +---- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt index 68f732c7..428f2c2b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt @@ -9,18 +9,16 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import com.sopt.clody.presentation.ui.auth.guide.navigation.guideScreen import com.sopt.clody.presentation.ui.auth.guide.navigation.navigateToGuide -import com.sopt.clody.presentation.ui.auth.login.navigation.loginScreen -import com.sopt.clody.presentation.ui.auth.login.navigation.navigateToLogin -import com.sopt.clody.presentation.ui.auth.signup.navigation.navigateToNickname -import com.sopt.clody.presentation.ui.auth.signup.navigation.navigateToTermsOfService -import com.sopt.clody.presentation.ui.auth.signup.navigation.nicknameScreen -import com.sopt.clody.presentation.ui.auth.signup.navigation.termsOfServiceScreen +import com.sopt.clody.presentation.ui.auth.signup.navigation.navigateToSignUp +import com.sopt.clody.presentation.ui.auth.signup.navigation.signUpScreen import com.sopt.clody.presentation.ui.auth.timereminder.navigateToTimeReminder import com.sopt.clody.presentation.ui.auth.timereminder.timeReminderScreen import com.sopt.clody.presentation.ui.diarylist.navigation.diaryListScreen import com.sopt.clody.presentation.ui.diarylist.navigation.navigateToDiaryList import com.sopt.clody.presentation.ui.home.navigation.homeScreen import com.sopt.clody.presentation.ui.home.navigation.navigateToHome +import com.sopt.clody.presentation.ui.login.navigation.loginScreen +import com.sopt.clody.presentation.ui.login.navigation.navigateToLogin import com.sopt.clody.presentation.ui.replydiary.navigation.navigateToReplyDiary import com.sopt.clody.presentation.ui.replydiary.navigation.replyDiaryScreen import com.sopt.clody.presentation.ui.replyloading.navigation.navigateToReplyLoading @@ -69,15 +67,11 @@ fun ClodyNavHost( ) loginScreen( - navigateToTermsOfService = navController::navigateToTermsOfService, + navigateToSignUp = navController::navigateToSignUp, navigateToHome = navController::navigateToHome, ) - termsOfServiceScreen( - navigateToNickname = navController::navigateToNickname, - navigateToLogin = navController::navigateToLogin, - ) - nicknameScreen( - navigateToReminder = navController::navigateToTimeReminder, + signUpScreen( + navigateToHome = navController::navigateToTimeReminder, navigateToPrevious = navController::safePopBackStack, ) timeReminderScreen( diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt index 4ae1aa8f..418de0cb 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt @@ -17,10 +17,7 @@ sealed interface Route { data object Login : Route @Serializable - data object TermsOfService : Route - - @Serializable - data object Nickname : Route + data object SignUp : Route @Serializable data object TimeReminder : Route From 2ffffb65287636879461ce23302f362713bb5a48 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:56:30 +0900 Subject: [PATCH 052/299] =?UTF-8?q?[ADD/#252]=20Mavericks=20ViewModel=20?= =?UTF-8?q?=EB=B0=94=EC=9D=B8=EB=94=A9=EC=9D=84=20=EC=9C=84=ED=95=9C=20Vie?= =?UTF-8?q?wModelsModule=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/di/ViewModelsModule.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/di/ViewModelsModule.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/di/ViewModelsModule.kt b/app/src/main/java/com/sopt/clody/presentation/di/ViewModelsModule.kt new file mode 100644 index 00000000..420e1f68 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/di/ViewModelsModule.kt @@ -0,0 +1,38 @@ +package com.sopt.clody.presentation.di + +import com.airbnb.mvrx.hilt.AssistedViewModelFactory +import com.airbnb.mvrx.hilt.MavericksViewModelComponent +import com.airbnb.mvrx.hilt.ViewModelKey +import com.sopt.clody.presentation.ui.auth.signup.SignUpViewModel +import com.sopt.clody.presentation.ui.login.LoginViewModel +import com.sopt.clody.presentation.ui.splash.SplashViewModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.IntoMap + +@Module +@InstallIn(MavericksViewModelComponent::class) +interface ViewModelsModule { + + @Binds + @IntoMap + @ViewModelKey(SplashViewModel::class) + fun bindSplashViewModelFactory( + factory: SplashViewModel.Factory, + ): AssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @ViewModelKey(LoginViewModel::class) + fun bindLoginViewModelFactory( + factory: LoginViewModel.Factory, + ): AssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @ViewModelKey(SignUpViewModel::class) + fun bindSignUpViewModelFactory( + factory: SignUpViewModel.Factory, + ): AssistedViewModelFactory<*, *> +} From 003da5244fa884d2e3cdb63ad2b62e98c09c47af Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:56:54 +0900 Subject: [PATCH 053/299] =?UTF-8?q?[REFACTOR/#252]=20Scaffold=EB=A5=BC=20B?= =?UTF-8?q?ox=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EC=97=AC=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/ui/main/ClodyApp.kt | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyApp.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyApp.kt index 8907a986..6a23e9ba 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyApp.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyApp.kt @@ -1,9 +1,8 @@ package com.sopt.clody.presentation.ui.main import android.content.Intent +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier @@ -26,14 +25,10 @@ fun ClodyApp( } } - Scaffold( - modifier = Modifier.fillMaxSize(), - content = { innerPadding -> - ClodyNavHost( - appState = appState, - modifier = Modifier.padding(innerPadding), - startIntent = startIntent, - ) - }, - ) + Box(modifier = Modifier.fillMaxSize()) { + ClodyNavHost( + appState = appState, + startIntent = startIntent, + ) + } } From f1625f2969b96c5f2c580d5825ba540a3191713d Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:57:35 +0900 Subject: [PATCH 054/299] =?UTF-8?q?[DEL/#252]=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=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 --- .../presentation/ui/auth/login/SignInState.kt | 7 - .../ui/auth/signup/NicknameScreen.kt | 232 ------------------ .../ui/auth/signup/SignUpState.kt | 7 - .../ui/auth/signup/TermsOfServiceScreen.kt | 217 ---------------- 4 files changed, 463 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/login/SignInState.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameScreen.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpState.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/TermsOfServiceScreen.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/SignInState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/SignInState.kt deleted file mode 100644 index 17fe8cce..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/login/SignInState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.sopt.clody.presentation.ui.auth.login - -import com.sopt.clody.presentation.utils.base.UiState - -data class SignInState( - val uiState: UiState = UiState.Empty, -) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameScreen.kt deleted file mode 100644 index dc17277e..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameScreen.kt +++ /dev/null @@ -1,232 +0,0 @@ -package com.sopt.clody.presentation.ui.auth.signup - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -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.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -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.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.sopt.clody.R -import com.sopt.clody.presentation.ui.auth.component.textfield.NickNameTextField -import com.sopt.clody.presentation.ui.component.LoadingScreen -import com.sopt.clody.presentation.ui.component.button.ClodyButton -import com.sopt.clody.presentation.ui.component.dialog.FailureDialog -import com.sopt.clody.presentation.utils.base.UiState -import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage -import com.sopt.clody.ui.theme.ClodyTheme - -@Composable -fun NicknameRoute( - navigateToReminder: () -> Unit, - navigateToPrevious: () -> Unit, - viewModel: SignUpViewModel = hiltViewModel(), -) { - val nickname by viewModel.nickname.collectAsState() - val isValidNickname by viewModel.isValidNickname.collectAsState() - val nicknameMessage by viewModel.nicknameMessage.collectAsState() - val signUpState by viewModel.signUpState.collectAsState() - val context = LocalContext.current - var showDialog by remember { mutableStateOf(false) } - var dialogMessage by remember { mutableStateOf("") } - - NicknameScreen( - nickname = nickname, - onNicknameChange = viewModel::setNickname, - onCompleteClick = { viewModel.proceedWithSignUp(context) }, - onBackClick = navigateToPrevious, - isLoading = signUpState.uiState is UiState.Loading, - isValidNickname = isValidNickname, - nicknameMessage = nicknameMessage, - ) - - LaunchedEffect(signUpState) { - when (val result = signUpState.uiState) { - is UiState.Success -> { - navigateToReminder() - } - - is UiState.Failure -> { - showDialog = true - dialogMessage = result.msg - } - - else -> {} - } - } - - if (showDialog) { - FailureDialog( - message = dialogMessage, - onDismiss = { - showDialog = false - viewModel.resetSignUpState() - }, - ) - } -} - -@Composable -fun NicknameScreen( - nickname: String, - onNicknameChange: (String) -> Unit, - onCompleteClick: () -> Unit, - onBackClick: () -> Unit, - isLoading: Boolean, - isValidNickname: Boolean, - nicknameMessage: String, -) { - var nicknameTextField by remember { mutableStateOf(TextFieldValue(nickname)) } - val focusRequester = remember { FocusRequester() } - var isFocused by remember { mutableStateOf(false) } - val focusManager = LocalFocusManager.current - - val annotatedString = buildAnnotatedString { - withStyle(style = SpanStyle(color = ClodyTheme.colors.gray04)) { - append("${nicknameTextField.text.length}") - } - withStyle(style = SpanStyle(color = ClodyTheme.colors.gray06)) { - append(" / ") - } - withStyle(style = SpanStyle(color = ClodyTheme.colors.gray06)) { - append("10") - } - } - - Scaffold( - topBar = { - IconButton( - onClick = { onBackClick() }, - modifier = Modifier - .statusBarsPadding() - .padding(start = 8.dp), - ) { - Image( - painter = painterResource(id = R.drawable.ic_nickname_back), - contentDescription = null, - ) - } - }, - bottomBar = { - ClodyButton( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(horizontal = 24.dp) - .padding(bottom = 28.dp) - .imePadding(), - onClick = { - focusManager.clearFocus() - onCompleteClick() - }, - text = stringResource(id = R.string.nickname_next), - enabled = nicknameTextField.text.isNotEmpty() && isValidNickname, - ) - }, - content = { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .background(color = ClodyTheme.colors.white) - .padding(paddingValues) - .padding(horizontal = 24.dp), - horizontalAlignment = Alignment.Start, - ) { - Spacer(modifier = Modifier.heightForScreenPercentage(0.056f)) - Text( - text = stringResource(id = R.string.nickname_title), - style = ClodyTheme.typography.head1, - color = ClodyTheme.colors.gray01, - ) - Spacer(modifier = Modifier.heightForScreenPercentage(0.06f)) - NickNameTextField( - value = nicknameTextField, - onValueChange = { - nicknameTextField = it - onNicknameChange(it.text) - }, - hint = stringResource(id = R.string.nickname_input_hint), - isFocused = isFocused, - isValid = isValidNickname, - onFocusChanged = { isFocused = it }, - onRemove = { nicknameTextField = TextFieldValue("") }, - modifier = Modifier - .focusRequester(focusRequester) - .clickable { focusRequester.requestFocus() } - .fillMaxWidth(), - ) - Spacer(modifier = Modifier.heightForScreenPercentage(0.005f)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = nicknameMessage, - style = ClodyTheme.typography.detail1Regular, - color = when { - nicknameTextField.text.isEmpty() -> ClodyTheme.colors.gray04 - isValidNickname -> ClodyTheme.colors.gray04 - else -> ClodyTheme.colors.red - }, - ) - Text( - text = annotatedString, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ClodyTheme.typography.detail1Medium, - ) - } - } - }, - ) - - if (isLoading) { - LoadingScreen() - } -} - -@Preview(showBackground = true) -@Composable -fun NicknameScreenPreview() { - NicknameScreen( - nickname = "닉네임", - onNicknameChange = {}, - onCompleteClick = {}, - onBackClick = {}, - isLoading = false, - isValidNickname = true, - nicknameMessage = "특수문자, 띄어쓰기 없이 작성해주세요", - ) -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpState.kt deleted file mode 100644 index e4d57f0b..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.sopt.clody.presentation.ui.auth.signup - -import com.sopt.clody.presentation.utils.base.UiState - -data class SignUpState( - val uiState: UiState = UiState.Empty, -) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/TermsOfServiceScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/TermsOfServiceScreen.kt deleted file mode 100644 index d9a5fffa..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/TermsOfServiceScreen.kt +++ /dev/null @@ -1,217 +0,0 @@ -package com.sopt.clody.presentation.ui.auth.signup - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -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.platform.LocalContext -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.sopt.clody.R -import com.sopt.clody.presentation.ui.auth.component.button.NextButton -import com.sopt.clody.presentation.ui.auth.component.checkbox.CustomCheckbox -import com.sopt.clody.presentation.ui.component.button.ClodyButton -import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider -import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls -import com.sopt.clody.presentation.ui.setting.screen.onClickSettingOption -import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage -import com.sopt.clody.ui.theme.ClodyTheme -import kotlinx.coroutines.delay - -@Composable -fun TermsOfServiceRoute( - navigateToNickname: () -> Unit, - navigateToLogin: () -> Unit, -) { - var backPressCount by remember { mutableStateOf(0) } - - LaunchedEffect(backPressCount) { - if (backPressCount > 0) { - delay(2000) - backPressCount = 0 - } - } - - BackHandler { - if (backPressCount == 1) { - navigateToLogin() - } else { - backPressCount++ - } - } - TermsOfServiceScreen( - onAgreeClick = navigateToNickname, - onBackClick = navigateToLogin, - ) -} - -@Composable -fun TermsOfServiceScreen( - onAgreeClick: () -> Unit, - onBackClick: () -> Unit, -) { - var allChecked by remember { mutableStateOf(false) } - var serviceChecked by remember { mutableStateOf(false) } - var privacyChecked by remember { mutableStateOf(false) } - - val isAgreeButtonEnabled = serviceChecked && privacyChecked - val context = LocalContext.current - - Scaffold( - topBar = { - IconButton( - onClick = { onBackClick() }, - modifier = Modifier - .statusBarsPadding() - .padding(start = 8.dp), - ) { - Image( - painter = painterResource(id = R.drawable.ic_nickname_back), - contentDescription = null, - ) - } - }, - bottomBar = { - ClodyButton( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(horizontal = 24.dp) - .padding(bottom = 28.dp), - onClick = onAgreeClick, - text = stringResource(id = R.string.terms_next), - enabled = isAgreeButtonEnabled, - ) - }, - content = { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .background(color = ClodyTheme.colors.white) - .padding(paddingValues) - .padding(horizontal = 24.dp), - ) { - Spacer(modifier = Modifier.heightForScreenPercentage(0.056f)) - Text( - text = stringResource(id = R.string.terms_title), - style = ClodyTheme.typography.head1, - color = ClodyTheme.colors.gray01, - ) - Spacer(modifier = Modifier.heightForScreenPercentage(0.06f)) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = stringResource(id = R.string.terms_agree_all), - style = ClodyTheme.typography.head3, - color = ClodyTheme.colors.gray01, - modifier = Modifier.weight(1f), - ) - CustomCheckbox( - checked = allChecked, - onCheckedChange = { checked -> - allChecked = checked - serviceChecked = checked - privacyChecked = checked - }, - size = 25.dp, - checkedImageRes = R.drawable.ic_terms_check_on_25, - uncheckedImageRes = R.drawable.ic_terms_check_off_25, - ) - } - - Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) - HorizontalDivider(color = ClodyTheme.colors.gray07, thickness = 1.dp) - Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth(), - ) { - Text( - stringResource(id = R.string.terms_service_use), - style = ClodyTheme.typography.body1Medium, - color = ClodyTheme.colors.gray01, - ) - NextButton( - onClick = { onClickSettingOption(context, SettingOptionUrls.TERMS_OF_SERVICE_URL) }, - imageResource = R.drawable.ic_terms_next, - contentDescription = null, - ) - Spacer(modifier = Modifier.weight(1f)) - CustomCheckbox( - checked = serviceChecked, - onCheckedChange = { checked -> - serviceChecked = checked - if (!checked) allChecked = false - if (checked && privacyChecked) allChecked = true - }, - size = 23.dp, - checkedImageRes = R.drawable.ic_terms_check_on_23, - uncheckedImageRes = R.drawable.ic_terms_check_off_23, - ) - } - - Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - stringResource(id = R.string.terms_service_privacy), - style = ClodyTheme.typography.body1Medium, - color = ClodyTheme.colors.gray01, - ) - NextButton( - onClick = { onClickSettingOption(context, SettingOptionUrls.PRIVACY_POLICY_URL) }, - imageResource = R.drawable.ic_terms_next, - contentDescription = null, - ) - Spacer(modifier = Modifier.weight(1f)) - CustomCheckbox( - checked = privacyChecked, - onCheckedChange = { checked -> - privacyChecked = checked - if (!checked) allChecked = false - if (checked && serviceChecked) allChecked = true - }, - size = 23.dp, - checkedImageRes = R.drawable.ic_terms_check_on_23, - uncheckedImageRes = R.drawable.ic_terms_check_off_23, - ) - } - } - }, - ) -} - -@Preview(showBackground = true) -@Composable -fun TermsOfServiceScreenPreview() { - TermsOfServiceScreen( - onAgreeClick = { }, - onBackClick = { }, - ) -} From 876044c8f52e1f13f43de175a8394516bb84b98f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 00:58:58 +0900 Subject: [PATCH 055/299] =?UTF-8?q?[MOD/#252]:=20CLODYTheme=20->=20ClodyTh?= =?UTF-8?q?eme=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 --- app/src/main/java/com/sopt/clody/ui/theme/Theme.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt b/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt index 9d5045f6..6b04409c 100644 --- a/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt +++ b/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable @Composable -fun CLODYTheme( +fun ClodyTheme( content: @Composable () -> Unit, ) { CompositionLocalProvider(content = content) From a61b83a4c8bd296ea5d39bb056751f9cbbac3663 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 01:03:13 +0900 Subject: [PATCH 056/299] =?UTF-8?q?[REFACTOR/#252]=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 광고 관련 클래스들을 ad 패키지로 이동 - calendar을 calendar 패키지로 이동 --- .../main/java/com/sopt/clody/core/{ => ad}/RewardAdShower.kt | 2 +- .../main/java/com/sopt/clody/data/ad/RewardAdShowerImpl.kt | 2 +- app/src/main/java/com/sopt/clody/di/AdModule.kt | 2 +- .../sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt | 2 +- .../presentation/ui/home/calendar/component/MonthlyItem.kt | 2 +- .../ui/home/{ => calendar}/model/CalendarDateData.kt | 2 +- .../ui/home/{ => calendar}/model/DiaryDateData.kt | 2 +- .../com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt | 2 +- .../sopt/clody/presentation/ui/home/screen/HomeViewModel.kt | 2 +- .../java/com/sopt/clody/presentation/ui/main/MainActivity.kt | 4 ++-- .../ui/replyloading/screen/ReplyLoadingViewModel.kt | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) rename app/src/main/java/com/sopt/clody/core/{ => ad}/RewardAdShower.kt (82%) rename app/src/main/java/com/sopt/clody/presentation/ui/home/{ => calendar}/model/CalendarDateData.kt (88%) rename app/src/main/java/com/sopt/clody/presentation/ui/home/{ => calendar}/model/DiaryDateData.kt (71%) diff --git a/app/src/main/java/com/sopt/clody/core/RewardAdShower.kt b/app/src/main/java/com/sopt/clody/core/ad/RewardAdShower.kt similarity index 82% rename from app/src/main/java/com/sopt/clody/core/RewardAdShower.kt rename to app/src/main/java/com/sopt/clody/core/ad/RewardAdShower.kt index cd9dc6c5..b85e9b79 100644 --- a/app/src/main/java/com/sopt/clody/core/RewardAdShower.kt +++ b/app/src/main/java/com/sopt/clody/core/ad/RewardAdShower.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.core +package com.sopt.clody.core.ad import android.app.Activity diff --git a/app/src/main/java/com/sopt/clody/data/ad/RewardAdShowerImpl.kt b/app/src/main/java/com/sopt/clody/data/ad/RewardAdShowerImpl.kt index 747ddfe1..54335e19 100644 --- a/app/src/main/java/com/sopt/clody/data/ad/RewardAdShowerImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/ad/RewardAdShowerImpl.kt @@ -3,7 +3,7 @@ package com.sopt.clody.data.ad import android.app.Activity import com.google.android.gms.ads.AdError import com.google.android.gms.ads.FullScreenContentCallback -import com.sopt.clody.core.RewardAdShower +import com.sopt.clody.core.ad.RewardAdShower import com.sopt.clody.data.remote.datasource.AdRemoteDataSource import javax.inject.Inject diff --git a/app/src/main/java/com/sopt/clody/di/AdModule.kt b/app/src/main/java/com/sopt/clody/di/AdModule.kt index 928e59cc..b6ed4393 100644 --- a/app/src/main/java/com/sopt/clody/di/AdModule.kt +++ b/app/src/main/java/com/sopt/clody/di/AdModule.kt @@ -1,6 +1,6 @@ package com.sopt.clody.di -import com.sopt.clody.core.RewardAdShower +import com.sopt.clody.core.ad.RewardAdShower import com.sopt.clody.data.ad.RewardAdShowerImpl import dagger.Binds import dagger.Module diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt index a47b4d40..6b2888e5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt @@ -17,7 +17,7 @@ import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.home.calendar.component.DailyDiaryListItem import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider import com.sopt.clody.presentation.ui.home.calendar.component.MonthlyItem -import com.sopt.clody.presentation.ui.home.model.generateCalendarDates +import com.sopt.clody.presentation.ui.home.calendar.model.generateCalendarDates import com.sopt.clody.presentation.ui.home.screen.DailyDiariesState import com.sopt.clody.presentation.ui.home.screen.HomeViewModel import java.time.LocalDate diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/MonthlyItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/MonthlyItem.kt index 90452006..55ae5155 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/MonthlyItem.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/MonthlyItem.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.dp import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.presentation.ui.home.model.CalendarDate +import com.sopt.clody.presentation.ui.home.calendar.model.CalendarDate import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import kotlinx.datetime.DayOfWeek diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/model/CalendarDateData.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/CalendarDateData.kt similarity index 88% rename from app/src/main/java/com/sopt/clody/presentation/ui/home/model/CalendarDateData.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/CalendarDateData.kt index 6264ba52..fcbbc34f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/model/CalendarDateData.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/CalendarDateData.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.home.model +package com.sopt.clody.presentation.ui.home.calendar.model import java.time.YearMonth diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/model/DiaryDateData.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/DiaryDateData.kt similarity index 71% rename from app/src/main/java/com/sopt/clody/presentation/ui/home/model/DiaryDateData.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/DiaryDateData.kt index 55585e33..b8d5b3aa 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/model/DiaryDateData.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/DiaryDateData.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.home.model +package com.sopt.clody.presentation.ui.home.calendar.model import java.time.LocalDate diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 1271ae2a..6afcf19b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -26,9 +26,9 @@ import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet import com.sopt.clody.presentation.ui.component.dialog.ClodyDialog import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet import com.sopt.clody.presentation.ui.component.timepicker.YearMonthPicker +import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData import com.sopt.clody.presentation.ui.home.component.DiaryStateButton import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar -import com.sopt.clody.presentation.ui.home.model.DiaryDateData import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.navigation.ReplyStatus diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index eb99e84d..86c97b0f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -6,7 +6,7 @@ import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository -import com.sopt.clody.presentation.ui.home.model.DiaryDateData +import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData import com.sopt.clody.presentation.utils.network.ErrorMessages import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/MainActivity.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/MainActivity.kt index adcf751f..a9e60661 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/main/MainActivity.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/MainActivity.kt @@ -7,7 +7,7 @@ import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import com.sopt.clody.ui.theme.CLODYTheme +import com.sopt.clody.ui.theme.ClodyTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -30,7 +30,7 @@ class MainActivity : ComponentActivity() { ) setContent { - CLODYTheme { + ClodyTheme { val appState = rememberClodyAppState() ClodyApp(appState = appState, startIntent = intent) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt index e885e820..3683b4a4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt @@ -3,7 +3,7 @@ package com.sopt.clody.presentation.ui.replyloading.screen import android.app.Activity import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sopt.clody.core.RewardAdShower +import com.sopt.clody.core.ad.RewardAdShower import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AdRepository From aebfde9babd94a9ab846fd15b382506265ae1ebd Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 28 May 2025 17:25:23 +0900 Subject: [PATCH 057/299] [REFACTOR/#260] ReplyStatus -> Domain Layer --- .../clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt | 2 +- .../utils/navigation => domain/model}/ReplyStatus.kt | 2 +- .../clody/presentation/ui/diarylist/component/DailyDiaryCard.kt | 2 +- .../presentation/ui/diarylist/component/MonthlyDiaryList.kt | 2 +- .../presentation/ui/diarylist/navigation/DiaryListNavigation.kt | 2 +- .../clody/presentation/ui/diarylist/screen/DiaryListScreen.kt | 2 +- .../clody/presentation/ui/home/navigation/HomeNavigation.kt | 2 +- .../com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt | 2 +- .../sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt | 2 +- .../ui/replydiary/navigation/ReplyDiaryNavigation.kt | 2 +- .../ui/replyloading/navigation/ReplyLoadingNavigation.kt | 2 +- .../presentation/ui/replyloading/screen/ReplyLoadingScreen.kt | 2 +- .../java/com/sopt/clody/presentation/utils/navigation/Route.kt | 1 + 13 files changed, 13 insertions(+), 12 deletions(-) rename app/src/main/java/com/sopt/clody/{presentation/utils/navigation => domain/model}/ReplyStatus.kt (70%) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt index 35533c1d..695d959c 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt @@ -1,6 +1,6 @@ package com.sopt.clody.data.remote.dto.response -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/ReplyStatus.kt b/app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt similarity index 70% rename from app/src/main/java/com/sopt/clody/presentation/utils/navigation/ReplyStatus.kt rename to app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt index d50e0e9c..bf97c6af 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/ReplyStatus.kt +++ b/app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.utils.navigation +package com.sopt.clody.domain.model import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt index df96bff7..bab9dad6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.ui.theme.ClodyTheme @Composable diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt index aa7f4c29..940fbc66 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.unit.dp import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel import com.sopt.clody.presentation.utils.extension.getDayOfWeek -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus @Composable fun MonthlyDiaryList( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt index 3b5c1235..dd111388 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt @@ -5,7 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListRoute -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.diaryListScreen( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt index 78b5edf9..6830b3c7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt @@ -24,7 +24,7 @@ import com.sopt.clody.presentation.ui.diarylist.component.EmptyDiaryList import com.sopt.clody.presentation.ui.diarylist.component.MonthlyDiaryList import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt index 1d002b15..fda47228 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt @@ -6,7 +6,7 @@ import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.sopt.clody.presentation.ui.home.screen.HomeRoute -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route import java.time.LocalDate diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 1271ae2a..d8692133 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -31,7 +31,7 @@ import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar import com.sopt.clody.presentation.ui.home.model.DiaryDateData import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme import java.time.LocalDate diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt index 75d92bd1..c5f9ad63 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt @@ -40,7 +40,7 @@ import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.ui.theme.ClodyTheme @Composable diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt index e06469bb..15a29792 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt @@ -6,7 +6,7 @@ import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.sopt.clody.presentation.ui.replydiary.ReplyDiaryRoute -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.replyDiaryScreen( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt index aa42a929..31490532 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt @@ -6,7 +6,7 @@ import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.sopt.clody.presentation.ui.replyloading.screen.ReplyLoadingRoute -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.replyLoadingScreen( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt index a3fd0b64..6f463472 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt @@ -46,7 +46,7 @@ import com.sopt.clody.presentation.ui.replyloading.component.QuickReplyAdButton import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage -import com.sopt.clody.presentation.utils.navigation.ReplyStatus +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.coroutines.delay diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt index 4ae1aa8f..2240904d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt @@ -1,5 +1,6 @@ package com.sopt.clody.presentation.utils.navigation +import com.sopt.clody.domain.model.ReplyStatus import kotlinx.serialization.Serializable /** From 2b8d331e5597775893ea54b26cd32e3e25fb45d4 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 28 May 2025 17:29:01 +0900 Subject: [PATCH 058/299] [FIX/#260] Ktlint formatting --- .../clody/presentation/ui/diarylist/component/DailyDiaryCard.kt | 2 +- .../presentation/ui/diarylist/component/MonthlyDiaryList.kt | 2 +- .../presentation/ui/diarylist/navigation/DiaryListNavigation.kt | 2 +- .../clody/presentation/ui/diarylist/screen/DiaryListScreen.kt | 2 +- .../clody/presentation/ui/home/navigation/HomeNavigation.kt | 2 +- .../com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt | 2 +- .../sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt | 2 +- .../ui/replydiary/navigation/ReplyDiaryNavigation.kt | 2 +- .../ui/replyloading/navigation/ReplyLoadingNavigation.kt | 2 +- .../presentation/ui/replyloading/screen/ReplyLoadingScreen.kt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt index bab9dad6..5872a044 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt @@ -28,8 +28,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto -import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel import com.sopt.clody.ui.theme.ClodyTheme @Composable diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt index 940fbc66..401257a1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt @@ -9,9 +9,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel import com.sopt.clody.presentation.utils.extension.getDayOfWeek -import com.sopt.clody.domain.model.ReplyStatus @Composable fun MonthlyDiaryList( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt index dd111388..fa10a207 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt @@ -4,8 +4,8 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListRoute import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListRoute import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.diaryListScreen( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt index 6830b3c7..15de33b5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet @@ -24,7 +25,6 @@ import com.sopt.clody.presentation.ui.diarylist.component.EmptyDiaryList import com.sopt.clody.presentation.ui.diarylist.component.MonthlyDiaryList import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils -import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt index fda47228..6cc39e2d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt @@ -5,8 +5,8 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.presentation.ui.home.screen.HomeRoute import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.home.screen.HomeRoute import com.sopt.clody.presentation.utils.navigation.Route import java.time.LocalDate diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index d8692133..b60c4983 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet @@ -31,7 +32,6 @@ import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar import com.sopt.clody.presentation.ui.home.model.DiaryDateData import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils -import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme import java.time.LocalDate diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt index c5f9ad63..c4aa168e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt @@ -35,12 +35,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage -import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.ui.theme.ClodyTheme @Composable diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt index 15a29792..f1e5b161 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt @@ -5,8 +5,8 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.presentation.ui.replydiary.ReplyDiaryRoute import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.replydiary.ReplyDiaryRoute import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.replyDiaryScreen( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt index 31490532..be1af5f9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt @@ -5,8 +5,8 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.presentation.ui.replyloading.screen.ReplyLoadingRoute import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.replyloading.screen.ReplyLoadingRoute import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.replyLoadingScreen( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt index 6f463472..cf221aad 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.airbnb.lottie.compose.LottieConstants import com.sopt.clody.R +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.button.ClodyButton @@ -46,7 +47,6 @@ import com.sopt.clody.presentation.ui.replyloading.component.QuickReplyAdButton import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage -import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.coroutines.delay From f6c82713a164ce1b7d8a4baa27052cb4ac8f5504 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 28 May 2025 17:54:26 +0900 Subject: [PATCH 059/299] trigger CI From 47533453f951fae0da79b9ac2e9ce7a839b90e55 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Thu, 29 May 2025 16:24:34 +0900 Subject: [PATCH 060/299] [FIX/#252] Ktlint formatting --- .../java/com/sopt/clody/presentation/utils/base/ClodyPreview.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/base/ClodyPreview.kt b/app/src/main/java/com/sopt/clody/presentation/utils/base/ClodyPreview.kt index c2315295..d6e8001a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/base/ClodyPreview.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/base/ClodyPreview.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes import com.sopt.clody.ui.theme.ClodyTheme // 아래 폴드는 예시이고 fontScale 같은 값도 조정이 가능합니다. -//@Preview(name = "Galaxy Z Fold3 접힌화면 (840x2289)", widthDp = 320, heightDp = 870, showBackground = true) +// @Preview(name = "Galaxy Z Fold3 접힌화면 (840x2289)", widthDp = 320, heightDp = 870, showBackground = true) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @PreviewScreenSizes annotation class ClodyPreview From b9284e7a9b16f284a58a654f6b16308855d2740e Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 2 Jun 2025 02:49:24 +0900 Subject: [PATCH 061/299] =?UTF-8?q?[REFACTOR/#252]=20allChecked=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20derived=20property=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=98=EC=97=AC=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=83=81=ED=83=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/presentation/ui/auth/signup/SignUpContract.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt index 22334a9b..4e9d003d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt @@ -14,8 +14,10 @@ class SignUpContract { val errorMessage: String? = null, val serviceChecked: Boolean = false, val privacyChecked: Boolean = false, - val allChecked: Boolean = false, ) : MavericksState { + val allChecked: Boolean + get() = serviceChecked && privacyChecked + enum class Step { TERMS, NICKNAME } From 734def9d89372e03d8794f76b3d415d5df654f2b Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 2 Jun 2025 02:50:10 +0900 Subject: [PATCH 062/299] =?UTF-8?q?[REFACTOR/#252]=20ClearError=20Intent?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EC=A1=B0=EA=B1=B4=EC=97=90=20is=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt index 3304a338..b16060ee 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -46,7 +46,7 @@ class LoginViewModel @AssistedInject constructor( private suspend fun handleIntent(intent: LoginContract.LoginIntent) { when (intent) { is LoginContract.LoginIntent.LoginWithKakao -> loginWithKakao(intent.context) - LoginContract.LoginIntent.ClearError -> setState { copy(errorMessage = null) } + is LoginContract.LoginIntent.ClearError -> setState { copy(errorMessage = null) } } } From 2dde0d15136dcc1ee935facb5a41b77033d9da68 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 2 Jun 2025 02:52:33 +0900 Subject: [PATCH 063/299] =?UTF-8?q?[REFACTOR/#252]=20=EC=95=B1=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=9D=90=EB=A6=84=EC=9D=84=20Int?= =?UTF-8?q?ent=20=EB=B0=8F=20SideEffect=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20?= =?UTF-8?q?ViewModel=20=ED=95=A8=EC=88=98=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Soft/Hard 업데이트 시 직접 처리하던 market 이동 및 종료 로직을 SideEffect로 분리 - HandleHardUpdate, HandleSoftUpdateConfirm Intent 추가 - NavigateToMarket, NavigateToMarketAndFinish, FinishApp SideEffect 추가 - ViewModel 내부에서 Intent를 처리하도록 구조 개선 - SplashRoute에서 UI 이벤트 발생 시 ViewModel에 Intent 전달하도록 변경 --- .../presentation/ui/splash/SplashContract.kt | 5 +++ .../presentation/ui/splash/SplashScreen.kt | 23 ++++++++-- .../presentation/ui/splash/SplashViewModel.kt | 42 ++++++++++++------- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt index f6a2bd4d..15ce091f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt @@ -13,11 +13,16 @@ class SplashContract { sealed class SplashIntent { data class InitSplash(val startIntent: Intent) : SplashIntent() + data class HandleHardUpdate(val isConfirm: Boolean) : SplashIntent() + data object HandleSoftUpdateConfirm : SplashIntent() data object ClearUpdateState : SplashIntent() } sealed interface SplashSideEffect { data object NavigateToLogin : SplashSideEffect data object NavigateToHome : SplashSideEffect + data object NavigateToMarket : SplashSideEffect + data object NavigateToMarketAndFinish : SplashSideEffect + data object FinishApp : SplashSideEffect } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt index ac080703..3e3b5ebd 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt @@ -50,6 +50,17 @@ fun SplashRoute( when (effect) { is SplashContract.SplashSideEffect.NavigateToLogin -> onLoginRequired() is SplashContract.SplashSideEffect.NavigateToHome -> onAlreadyLoggedIn() + is SplashContract.SplashSideEffect.NavigateToMarketAndFinish -> { + AppUpdateUtils.navigateToMarketAndFinish(activity) + } + + is SplashContract.SplashSideEffect.FinishApp -> { + activity.finishAffinity() + } + + is SplashContract.SplashSideEffect.NavigateToMarket -> { + AppUpdateUtils.navigateToMarket(context) + } } } } @@ -60,15 +71,21 @@ fun SplashRoute( SoftUpdateDialog( latestVersion = updateState.latestVersion, onDismiss = { viewModel.postIntent(SplashContract.SplashIntent.ClearUpdateState) }, - onConfirm = { AppUpdateUtils.navigateToMarket(context) }, + onConfirm = { + viewModel.postIntent(SplashContract.SplashIntent.HandleSoftUpdateConfirm) + }, ) } is AppUpdateState.HardUpdate -> { HardUpdateDialog( latestVersion = updateState.latestVersion, - onConfirm = { AppUpdateUtils.navigateToMarketAndFinish(activity) }, - onExit = { activity.finishAffinity() }, + onConfirm = { + viewModel.postIntent(SplashContract.SplashIntent.HandleHardUpdate(isConfirm = true)) + }, + onExit = { + viewModel.postIntent(SplashContract.SplashIntent.HandleHardUpdate(isConfirm = false)) + }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt index d8c3b746..327544d4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt @@ -45,18 +45,19 @@ class SplashViewModel @AssistedInject constructor( private suspend fun handleIntent(intent: SplashContract.SplashIntent) { when (intent) { - is SplashContract.SplashIntent.InitSplash -> { - if (intent.startIntent.hasExtra("google.message_id")) { - AmplitudeUtils.trackEvent(AmplitudeConstraints.ALARM) - } - attemptAutoLogin() - checkVersionAndNavigate() - } + is SplashContract.SplashIntent.InitSplash -> handleInitSplash(intent) + is SplashContract.SplashIntent.HandleHardUpdate -> handleHardUpdate(intent) + is SplashContract.SplashIntent.HandleSoftUpdateConfirm -> handleSoftUpdateConfirm() + is SplashContract.SplashIntent.ClearUpdateState -> clearUpdateState() + } + } - SplashContract.SplashIntent.ClearUpdateState -> { - setState { copy(updateState = AppUpdateState.Latest) } - } + private fun handleInitSplash(intent: SplashContract.SplashIntent.InitSplash) { + if (intent.startIntent.hasExtra("google.message_id")) { + AmplitudeUtils.trackEvent(AmplitudeConstraints.ALARM) } + attemptAutoLogin() + checkVersionAndNavigate() } private fun attemptAutoLogin() { @@ -72,10 +73,7 @@ class SplashViewModel @AssistedInject constructor( if (updateState == AppUpdateState.Latest) { delay(1000) - - val isLoggedIn = withState(this@SplashViewModel) { - it.isUserLoggedIn - } + val isLoggedIn = withState(this@SplashViewModel) { it.isUserLoggedIn } if (isLoggedIn == true) { _sideEffects.send(SplashContract.SplashSideEffect.NavigateToHome) @@ -86,6 +84,22 @@ class SplashViewModel @AssistedInject constructor( } } + private suspend fun handleHardUpdate(intent: SplashContract.SplashIntent.HandleHardUpdate) { + if (intent.isConfirm) { + _sideEffects.send(SplashContract.SplashSideEffect.NavigateToMarketAndFinish) + } else { + _sideEffects.send(SplashContract.SplashSideEffect.FinishApp) + } + } + + private suspend fun handleSoftUpdateConfirm() { + _sideEffects.send(SplashContract.SplashSideEffect.NavigateToMarket) + } + + private fun clearUpdateState() { + setState { copy(updateState = AppUpdateState.Latest) } + } + @AssistedFactory interface Factory : AssistedViewModelFactory { override fun create(state: SplashContract.SplashState): SplashViewModel From 72489f8d3a5a8c94e6bc14424ff24a18042bd951 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 2 Jun 2025 03:00:30 +0900 Subject: [PATCH 064/299] =?UTF-8?q?[REFACTOR/#252]=20allChecked=EA=B0=80?= =?UTF-8?q?=20derived=20property=EA=B0=80=20=EB=90=A8=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=EC=BD=94=EB=93=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/auth/signup/SignUpViewModel.kt | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index d3a9acb8..67f84559 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -89,7 +89,6 @@ class SignUpViewModel @AssistedInject constructor( is SignUpContract.SignUpIntent.ToggleAllChecked -> { setState { copy( - allChecked = intent.checked, serviceChecked = intent.checked, privacyChecked = intent.checked, ) @@ -98,21 +97,13 @@ class SignUpViewModel @AssistedInject constructor( is SignUpContract.SignUpIntent.ToggleServiceChecked -> { setState { - val all = intent.checked && this.privacyChecked - copy( - serviceChecked = intent.checked, - allChecked = all, - ) + copy(serviceChecked = intent.checked) } } is SignUpContract.SignUpIntent.TogglePrivacyChecked -> { setState { - val all = this.serviceChecked && intent.checked - copy( - privacyChecked = intent.checked, - allChecked = all, - ) + copy(privacyChecked = intent.checked) } } From ceadb1c601fe8d9a9556714ef6a888068a8efd3f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 2 Jun 2025 03:02:14 +0900 Subject: [PATCH 065/299] =?UTF-8?q?[REFACTOR/#252]=20handleIntent=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC=20=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/signup/SignUpViewModel.kt | 117 ++++++++---------- 1 file changed, 55 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index 67f84559..61ef7585 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -49,79 +49,72 @@ class SignUpViewModel @AssistedInject constructor( private suspend fun handleIntent(intent: SignUpContract.SignUpIntent) { when (intent) { - is SignUpContract.SignUpIntent.SetNickname -> { - val isValid = validateNickname(intent.value) - setState { - copy( - nickname = intent.value, - isValidNickname = isValid, - nicknameMessage = if (intent.value.isEmpty()) { - DEFAULT_NICKNAME_MESSAGE - } else if (isValid) { - DEFAULT_NICKNAME_MESSAGE - } else { - "사용할 수 없는 닉네임이에요" - }, - ) - } - } - - is SignUpContract.SignUpIntent.SetNicknameFocus -> { - setState { copy(isNicknameFocused = intent.isFocused) } - } + is SignUpContract.SignUpIntent.SetNickname -> handleSetNickname(intent) + is SignUpContract.SignUpIntent.SetNicknameFocus -> handleSetNicknameFocus(intent) + is SignUpContract.SignUpIntent.ProceedTerms -> handleProceedTerms() + is SignUpContract.SignUpIntent.CompleteSignUp -> signUp(intent.context) + is SignUpContract.SignUpIntent.ClearError -> clearError() + is SignUpContract.SignUpIntent.ToggleAllChecked -> handleToggleAllChecked(intent) + is SignUpContract.SignUpIntent.ToggleServiceChecked -> handleToggleServiceChecked(intent) + is SignUpContract.SignUpIntent.TogglePrivacyChecked -> handleTogglePrivacyChecked(intent) + SignUpContract.SignUpIntent.BackToTerms -> handleBackToTerms() + } + } - SignUpContract.SignUpIntent.ProceedTerms -> { - setState { copy(currentStep = SignUpContract.SignUpState.Step.NICKNAME) } - } + private fun handleSetNickname(intent: SignUpContract.SignUpIntent.SetNickname) { + val isValid = validateNickname(intent.value) + setState { + copy( + nickname = intent.value, + isValidNickname = isValid, + nicknameMessage = if (intent.value.isEmpty() || isValid) { + DEFAULT_NICKNAME_MESSAGE + } else { + "사용할 수 없는 닉네임이에요" + }, + ) + } + } - is SignUpContract.SignUpIntent.CompleteSignUp -> { - signUp(intent.context) - } + private fun handleSetNicknameFocus(intent: SignUpContract.SignUpIntent.SetNicknameFocus) { + setState { copy(isNicknameFocused = intent.isFocused) } + } - SignUpContract.SignUpIntent.ProceedTerms -> { - setState { copy(currentStep = SignUpContract.SignUpState.Step.NICKNAME) } - } + private fun handleProceedTerms() { + setState { copy(currentStep = SignUpContract.SignUpState.Step.NICKNAME) } + } - SignUpContract.SignUpIntent.ClearError -> { - setState { copy(errorMessage = null) } - } + private fun clearError() { + setState { copy(errorMessage = null) } + } - is SignUpContract.SignUpIntent.ToggleAllChecked -> { - setState { - copy( - serviceChecked = intent.checked, - privacyChecked = intent.checked, - ) - } - } + private fun handleToggleAllChecked(intent: SignUpContract.SignUpIntent.ToggleAllChecked) { + setState { + copy(serviceChecked = intent.checked, privacyChecked = intent.checked) + } + } - is SignUpContract.SignUpIntent.ToggleServiceChecked -> { - setState { - copy(serviceChecked = intent.checked) - } - } + private fun handleToggleServiceChecked(intent: SignUpContract.SignUpIntent.ToggleServiceChecked) { + setState { copy(serviceChecked = intent.checked) } + } - is SignUpContract.SignUpIntent.TogglePrivacyChecked -> { - setState { - copy(privacyChecked = intent.checked) - } - } + private fun handleTogglePrivacyChecked(intent: SignUpContract.SignUpIntent.TogglePrivacyChecked) { + setState { copy(privacyChecked = intent.checked) } + } - SignUpContract.SignUpIntent.BackToTerms -> { - setState { - copy( - currentStep = SignUpContract.SignUpState.Step.TERMS, - nickname = "", - isNicknameFocused = false, - isValidNickname = true, - nicknameMessage = SignUpContract.DEFAULT_NICKNAME_MESSAGE, - ) - } - } + private fun handleBackToTerms() { + setState { + copy( + currentStep = SignUpContract.SignUpState.Step.TERMS, + nickname = "", + isNicknameFocused = false, + isValidNickname = true, + nicknameMessage = SignUpContract.DEFAULT_NICKNAME_MESSAGE, + ) } } - fun signUp(context: Context) { + private fun signUp(context: Context) { viewModelScope.launch { val state = withState(this@SignUpViewModel) { it } if (!networkUtil.isNetworkAvailable()) { From 28bc445c01cab8ea0da612c9c2880bd210fa8e7c Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 2 Jun 2025 14:33:20 +0900 Subject: [PATCH 066/299] =?UTF-8?q?[FEAT/#266]=20=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=EC=95=8C=EB=A6=BC=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=98=EC=97=AC=20Dto=EB=A5=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=98=EA=B3=A0,=20UI=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/SendNotificationRequestDto.kt | 1 + .../response/NotificationInfoResponseDto.kt | 1 + .../response/SendNotificationResponseDto.kt | 1 + .../timereminder/TimeReminderViewModel.kt | 1 + .../component/DraftAlarmSwitch.kt | 66 +++++++++++++++++++ .../screen/DraftAlarmChangeState.kt | 10 +++ .../screen/NotificationSettingScreen.kt | 13 +++- .../screen/NotificationSettingViewModel.kt | 45 ++++++++++++- app/src/main/res/values/strings.xml | 1 + 9 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DraftAlarmSwitch.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DraftAlarmChangeState.kt diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/request/SendNotificationRequestDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/request/SendNotificationRequestDto.kt index 8a051a8d..3b09e539 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/request/SendNotificationRequestDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/request/SendNotificationRequestDto.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable @Serializable data class SendNotificationRequestDto( @SerialName("isDiaryAlarm") val isDiaryAlarm: Boolean, + @SerialName("isDraftAlarm") val isDraftAlarm: Boolean, @SerialName("isReplyAlarm") val isReplyAlarm: Boolean, @SerialName("time") val time: String, @SerialName("fcmToken") val fcmToken: String, diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/NotificationInfoResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/NotificationInfoResponseDto.kt index 641a8ca2..d7020108 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/NotificationInfoResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/NotificationInfoResponseDto.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable @Serializable data class NotificationInfoResponseDto( @SerialName("isDiaryAlarm") val isDiaryAlarm: Boolean, + @SerialName("isDraftAlarm") val isDraftAlarm: Boolean, @SerialName("isReplyAlarm") val isReplyAlarm: Boolean, @SerialName("time") val time: String, ) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/SendNotificationResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/SendNotificationResponseDto.kt index 4c29fc62..44e0e760 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/SendNotificationResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/SendNotificationResponseDto.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable @Serializable data class SendNotificationResponseDto( @SerialName("isDiaryAlarm") val isDiaryAlarm: Boolean, + @SerialName("isDraftAlarm") val isDraftAlarm: Boolean, @SerialName("isReplyAlarm") val isReplyAlarm: Boolean, @SerialName("time") val time: String, @SerialName("fcmToken") val fcmToken: String, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt index 91906f23..ee63c6de 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt @@ -45,6 +45,7 @@ class TimeReminderViewModel @Inject constructor( val requestDto = SendNotificationRequestDto( isDiaryAlarm = isPermissionGranted, + isDraftAlarm = isPermissionGranted, isReplyAlarm = isPermissionGranted, time = selectedTime, fcmToken = fcmToken, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DraftAlarmSwitch.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DraftAlarmSwitch.kt new file mode 100644 index 00000000..067d1a49 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DraftAlarmSwitch.kt @@ -0,0 +1,66 @@ +package com.sopt.clody.presentation.ui.setting.notificationsetting.component + +import android.content.Context +import androidx.compose.foundation.Image +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.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.sopt.clody.R +import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto +import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationSettingViewModel +import com.sopt.clody.ui.theme.ClodyTheme + +@Composable +fun DraftAlarmSwitch( + notificationSettingViewModel: NotificationSettingViewModel, + context: Context, + title: String, + notificationInfo: NotificationInfoResponseDto, + checkedState: MutableState, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = title, + style = ClodyTheme.typography.body1Medium, + color = ClodyTheme.colors.gray03, + ) + Spacer(modifier = Modifier.weight(1f)) + Switch( + checked = checkedState.value, + onCheckedChange = { + checkedState.value = it + notificationSettingViewModel.changeDraftAlarm(context, notificationInfo, checkedState.value) + }, + modifier = Modifier.scale(0.8f), + thumbContent = { + Image( + painter = painterResource(id = R.drawable.ic_notification_setting_switch_thumb), + contentDescription = null, + ) + }, + colors = SwitchDefaults.colors( + checkedThumbColor = ClodyTheme.colors.white, + checkedTrackColor = ClodyTheme.colors.mainYellow, + uncheckedThumbColor = ClodyTheme.colors.white, + uncheckedTrackColor = ClodyTheme.colors.gray06, + uncheckedBorderColor = ClodyTheme.colors.gray06, + ), + ) + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DraftAlarmChangeState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DraftAlarmChangeState.kt new file mode 100644 index 00000000..928a5620 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DraftAlarmChangeState.kt @@ -0,0 +1,10 @@ +package com.sopt.clody.presentation.ui.setting.notificationsetting.screen + +import com.sopt.clody.data.remote.dto.response.SendNotificationResponseDto + +sealed class DraftAlarmChangeState { + data object Idle : DraftAlarmChangeState() + data object Loading : DraftAlarmChangeState() + data class Success(val data: SendNotificationResponseDto) : DraftAlarmChangeState() + data class Failure(val errorMessage: String) : DraftAlarmChangeState() +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index 68848d9c..184aaba3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -30,6 +30,7 @@ import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage import com.sopt.clody.presentation.ui.setting.component.SettingTopAppBar import com.sopt.clody.presentation.ui.setting.notificationsetting.component.DiaryAlarmSwitch +import com.sopt.clody.presentation.ui.setting.notificationsetting.component.DraftAlarmSwitch import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSettingTime import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSettingTimePicker import com.sopt.clody.presentation.ui.setting.notificationsetting.component.ReplyAlarmSwitch @@ -43,6 +44,7 @@ fun NotificationSettingRoute( val context = LocalContext.current val notificationInfoState by notificationSettingViewModel.notificationInfoState.collectAsState() val diaryAlarmChangeState by notificationSettingViewModel.diaryAlarmChangeState.collectAsState() + val draftAlarmChangeState by notificationSettingViewModel.draftAlarmChangeState.collectAsState() val notificationTimeChangeState by notificationSettingViewModel.notificationTimeChangeState.collectAsState() val replyAlarmChangeState by notificationSettingViewModel.replyAlarmChangeState.collectAsState() val showFailureDialog by notificationSettingViewModel.showFailureDialog.collectAsState() @@ -54,8 +56,9 @@ fun NotificationSettingRoute( notificationSettingViewModel.getNotificationInfo() } - LaunchedEffect(diaryAlarmChangeState, notificationTimeChangeState, replyAlarmChangeState) { + LaunchedEffect(diaryAlarmChangeState, draftAlarmChangeState, notificationTimeChangeState, replyAlarmChangeState) { val isSuccess = diaryAlarmChangeState is DiaryAlarmChangeState.Success || + draftAlarmChangeState is DraftAlarmChangeState.Success || notificationTimeChangeState is NotificationTimeChangeState.Success || replyAlarmChangeState is ReplyAlarmChangeState.Success @@ -126,6 +129,14 @@ fun NotificationSettingScreen( checkedState = remember { mutableStateOf(notificationInfo.isDiaryAlarm) }, ) Spacer(modifier = Modifier.height(32.dp)) + DraftAlarmSwitch( + notificationSettingViewModel = notificationSettingViewModel, + context = context, + title = stringResource(R.string.notification_setting_draft_diary), + notificationInfo = notificationInfo, + checkedState = remember { mutableStateOf(notificationInfo.isDraftAlarm) }, + ) + Spacer(modifier = Modifier.height(32.dp)) NotificationSettingTime( selectedTime = notificationTime, updateNotificationTimePicker = updateNotificationTimePicker, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt index d1f27dd6..0bf7a3d1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt @@ -28,9 +28,10 @@ class NotificationSettingViewModel @Inject constructor( private val _diaryAlarmChangeState = MutableStateFlow(DiaryAlarmChangeState.Idle) val diaryAlarmChangeState: StateFlow = _diaryAlarmChangeState - private val _notificationTimeChangeState = MutableStateFlow( - NotificationTimeChangeState.Idle, - ) + private val _draftAlarmChangeState = MutableStateFlow(DraftAlarmChangeState.Idle) + val draftAlarmChangeState: StateFlow = _draftAlarmChangeState + + private val _notificationTimeChangeState = MutableStateFlow(NotificationTimeChangeState.Idle) val notificationTimeChangeState: StateFlow = _notificationTimeChangeState private val _replyAlarmChangeState = MutableStateFlow(ReplyAlarmChangeState.Idle) @@ -90,6 +91,42 @@ class NotificationSettingViewModel @Inject constructor( } val requestDto = SendNotificationRequestDto( isDiaryAlarm = diaryAlarm, + isDraftAlarm = notificationInfo.isDraftAlarm, + isReplyAlarm = notificationInfo.isReplyAlarm, + time = notificationInfo.time, + fcmToken = fcmToken, + ) + notificationRepository.sendNotification(requestDto).fold( + onSuccess = { _diaryAlarmChangeState.value = DiaryAlarmChangeState.Success(it) }, + onFailure = { + _failureDialogMessage.value = if (it.message?.contains("200") == false) { + FAILURE_TEMPORARY_MESSAGE + } else { + UNKNOWN_ERROR + } + _showFailureDialog.value = true + DiaryAlarmChangeState.Failure(_failureDialogMessage.value) + }, + ) + } + } + + fun changeDraftAlarm(context: Context, notificationInfo: NotificationInfoResponseDto, draftAlarm: Boolean) { + _notificationTimeChangeState.value = NotificationTimeChangeState.Loading + viewModelScope.launch { + if (!networkUtil.isNetworkAvailable()) { + _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + _showFailureDialog.value = true + return@launch + } + val fcmToken = getTokenFromPreferences(context) + if (fcmToken.isNullOrBlank()) { + _diaryAlarmChangeState.value = DiaryAlarmChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") + return@launch + } + val requestDto = SendNotificationRequestDto( + isDiaryAlarm = notificationInfo.isDiaryAlarm, + isDraftAlarm = draftAlarm, isReplyAlarm = notificationInfo.isReplyAlarm, time = notificationInfo.time, fcmToken = fcmToken, @@ -124,6 +161,7 @@ class NotificationSettingViewModel @Inject constructor( } val requestDto = SendNotificationRequestDto( isDiaryAlarm = notificationInfo.isDiaryAlarm, + isDraftAlarm = notificationInfo.isDraftAlarm, isReplyAlarm = notificationInfo.isReplyAlarm, time = time, fcmToken = fcmToken, @@ -158,6 +196,7 @@ class NotificationSettingViewModel @Inject constructor( } val requestDto = SendNotificationRequestDto( isDiaryAlarm = notificationInfo.isDiaryAlarm, + isDraftAlarm = notificationInfo.isDraftAlarm, isReplyAlarm = replyAlarm, time = notificationInfo.time, fcmToken = fcmToken, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54533794..c442ffbd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,6 +127,7 @@ 알림 설정 일기 작성 알림 받기 + 이어쓰기 알림 받기 알림 시간 답장 도착 알림 받기 완료 From c039ab0a723699ea2a2ba7a4f08ff7e7dfa8f968 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 17:34:54 +0900 Subject: [PATCH 067/299] =?UTF-8?q?[REFACTOR/#263]=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=ED=98=B8=EC=9D=B4=EC=8A=A4=ED=8C=85=EC=9C=BC=EB=A1=9C=20screen?= =?UTF-8?q?=EC=97=90=EC=84=9C=20ViewModel=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/writediary/screen/WriteDiaryScreen.kt | 221 +++++++----------- 1 file changed, 88 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index a931a59e..50da8b1f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -1,24 +1,17 @@ package com.sopt.clody.presentation.ui.writediary.screen -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -28,15 +21,13 @@ import androidx.compose.runtime.getValue 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.platform.LocalFocusManager -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 androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.LoadingScreen -import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.dialog.ClodyDialog import com.sopt.clody.presentation.ui.component.dialog.FailureDialog import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage @@ -44,10 +35,9 @@ import com.sopt.clody.presentation.ui.writediary.component.bottomsheet.DeleteWri import com.sopt.clody.presentation.ui.writediary.component.text.DiaryTitleText import com.sopt.clody.presentation.ui.writediary.component.textfield.WriteDiaryTextField import com.sopt.clody.presentation.ui.writediary.component.tooltip.TooltipIcon -import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints -import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.getDayOfWeek import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage +import com.sopt.clody.ui.theme.CLODYTheme import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -55,11 +45,14 @@ fun WriteDiaryRoute( year: Int, month: Int, date: Int, - navigateToReplyLoading: (year: Int, month: Int, date: Int) -> Unit, - navigateToHome: (year: Int, month: Int) -> Unit, + navigateToReplyLoading: (Int, Int, Int) -> Unit, + navigateToHome: (Int, Int) -> Unit, navigateToPrevious: () -> Unit, viewModel: WriteDiaryViewModel = hiltViewModel(), ) { + val writeDiaryState by viewModel.writeDiaryState.collectAsState() + val showFailureDialog by viewModel.showFailureDialog.collectAsState() + val failureMessage by viewModel.failureMessage.collectAsState() val entries = viewModel.entries val showWarnings = viewModel.showWarnings val showLimitMessage by viewModel::showLimitMessage @@ -67,11 +60,8 @@ fun WriteDiaryRoute( val showDeleteBottomSheet by viewModel::showDeleteBottomSheet val entryToDelete by viewModel::entryToDelete val showDialog by viewModel::showDialog - val writeDiaryState by viewModel.writeDiaryState.collectAsState() - val showFailureDialog by viewModel.showFailureDialog.collectAsState() - val failureMessage by viewModel.failureMessage.collectAsState() - val allFieldsEmpty by remember { + val allFieldsEmpty by remember(entries) { derivedStateOf { entries.all { it.isEmpty() } } } @@ -85,7 +75,6 @@ fun WriteDiaryRoute( } WriteDiaryScreen( - viewModel = viewModel, isLoading = writeDiaryState is WriteDiaryState.Loading, entries = entries, showWarnings = showWarnings, @@ -93,13 +82,36 @@ fun WriteDiaryRoute( showEmptyFieldsMessage = showEmptyFieldsMessage, showDeleteBottomSheet = showDeleteBottomSheet, entryToDelete = entryToDelete, - allFieldsEmpty = allFieldsEmpty, showDialog = showDialog, - onClickBack = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.WRITING_DIARY_BACK) - navigateToPrevious() + allFieldsEmpty = allFieldsEmpty, + onClickBack = navigateToPrevious, + onClickAdd = viewModel::addEntry, + onClickRemove = { index -> + viewModel.setEntryToDeleteIndex(index) + viewModel.updateShowDeleteBottomSheet(true) + }, + onConfirmDelete = { + if (entryToDelete != -1) viewModel.removeEntry(entryToDelete) }, - onCompleteClick = { viewModel.writeDiary(year, month, date, entries) }, + onDismissDelete = { viewModel.updateShowDeleteBottomSheet(false) }, + onTextChange = { index, text -> + viewModel.updateEntry(index, text) + viewModel.validateEntry(index, text) + }, + onClickComplete = { + viewModel.validateEntries() + if (showWarnings.all { !it }) { + if (entries.size > 1 && entries.any { it.isEmpty() }) { + viewModel.updateShowEmptyFieldsMessage(true) + } else { + viewModel.updateShowDialog(true) + } + } + }, + onConfirmDialog = { viewModel.writeDiary(year, month, date, entries) }, + onDismissDialog = { viewModel.updateShowDialog(false) }, + onDismissLimitMessage = { viewModel.updateShowLimitMessage(false) }, + onDismissEmptyFieldsMessage = { viewModel.updateShowEmptyFieldsMessage(false) }, year = year, month = month, day = date, @@ -108,14 +120,13 @@ fun WriteDiaryRoute( if (showFailureDialog) { FailureDialog( message = failureMessage, - onDismiss = { viewModel.resetFailureDialog() }, + onDismiss = viewModel::resetFailureDialog, ) } } @Composable fun WriteDiaryScreen( - viewModel: WriteDiaryViewModel, isLoading: Boolean, entries: List, showWarnings: List, @@ -123,10 +134,19 @@ fun WriteDiaryScreen( showEmptyFieldsMessage: Boolean, showDeleteBottomSheet: Boolean, entryToDelete: Int, - allFieldsEmpty: Boolean, showDialog: Boolean, + allFieldsEmpty: Boolean, onClickBack: () -> Unit, - onCompleteClick: () -> Unit, + onClickAdd: () -> Unit, + onClickRemove: (Int) -> Unit, + onConfirmDelete: () -> Unit, + onDismissDelete: () -> Unit, + onTextChange: (Int, String) -> Unit, + onClickComplete: () -> Unit, + onConfirmDialog: () -> Unit, + onDismissDialog: () -> Unit, + onDismissLimitMessage: (Boolean) -> Unit, + onDismissEmptyFieldsMessage: (Boolean) -> Unit, year: Int, month: Int, day: Int, @@ -135,88 +155,7 @@ fun WriteDiaryScreen( Scaffold( topBar = { - IconButton( - onClick = onClickBack, - modifier = Modifier - .statusBarsPadding() - .padding(top = 26.dp) - .padding(start = 12.dp), - ) { - Image( - painter = painterResource(id = R.drawable.ic_nickname_back), - contentDescription = null, - ) - } - }, - bottomBar = { - Box( - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .background(Color.Transparent), - contentAlignment = Alignment.Center, - ) { - Column( - modifier = Modifier.align(Alignment.BottomCenter), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(end = 24.dp) - .padding(bottom = 28.dp), - contentAlignment = Alignment.CenterEnd, - ) { - IconButton( - onClick = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.WRITING_DIARY_ADD_LIST) - if (entries.size < 5) { - viewModel.addEntry() - } - }, - enabled = entries.size < 5, - modifier = Modifier - .background( - color = if (entries.size < 5) ClodyTheme.colors.gray02 else ClodyTheme.colors.gray06, - shape = RoundedCornerShape(10.dp), - ) - .size(41.dp), - ) { - Image( - painter = painterResource(id = R.drawable.ic_writediary_add), - contentDescription = "Add", - ) - } - } - - ClodyButton( - onClick = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.WRITING_DIARY_COMPLETE) - viewModel.validateEntries() - if (showWarnings.all { !it }) { - if (entries.size > 1 && entries.any { it.isEmpty() }) { - viewModel.updateShowEmptyFieldsMessage(true) - } else { - viewModel.updateShowDialog(true) - } - } - }, - text = stringResource(R.string.write_diary_confirm_button), - enabled = !allFieldsEmpty, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(bottom = 28.dp), - ) - } - ShowToastMessages( - showLimitMessage = showLimitMessage, - showEmptyFieldsMessage = showEmptyFieldsMessage, - onShowLimitMessageChange = { viewModel.updateShowLimitMessage(it) }, - onShowEmptyFieldsMessageChange = { viewModel.updateShowEmptyFieldsMessage(it) }, - modifier = Modifier.align(Alignment.Center), - ) - } }, content = { innerPadding -> Column( @@ -252,21 +191,12 @@ fun WriteDiaryScreen( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - itemsIndexed( - items = entries, - key = { index, _ -> index }, - ) { index, text -> + itemsIndexed(entries, key = { index, _ -> index }) { index, text -> WriteDiaryTextField( entryNumber = index + 1, text = text, - onTextChange = { newText -> - viewModel.updateEntry(index, newText) - viewModel.validateEntry(index, newText) - }, - onRemove = { - viewModel.setEntryToDeleteIndex(index) - viewModel.updateShowDeleteBottomSheet(true) - }, + onTextChange = { newText -> onTextChange(index, newText) }, + onRemove = { onClickRemove(index) }, isRemovable = entries.size > 1, maxLength = 50, showWarning = showWarnings[index], @@ -276,27 +206,19 @@ fun WriteDiaryScreen( if (showDeleteBottomSheet) { DeleteWriteDiaryBottomSheet( - onDismissRequest = { viewModel.updateShowDeleteBottomSheet(false) }, - onDeleteConfirm = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.WRITING_DIARY_DELETE_LIST) - if (entryToDelete != -1) { - viewModel.removeEntry(entryToDelete) - } - }, + onDismissRequest = onDismissDelete, + onDeleteConfirm = onConfirmDelete, ) } if (showDialog) { ClodyDialog( - onDismiss = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.WRITING_DIARY_NO_COMPLETE) - viewModel.updateShowDialog(false) - }, + onDismiss = onDismissDialog, titleMassage = stringResource(R.string.write_diary_dialog_title), descriptionMassage = stringResource(R.string.write_diary_dialog_description), confirmOption = stringResource(R.string.write_diary_dialog_confirm_option), dismissOption = stringResource(R.string.write_diary_dialog_dismiss_option), - confirmAction = { onCompleteClick() }, + confirmAction = onConfirmDialog, confirmButtonColor = ClodyTheme.colors.mainYellow, confirmButtonTextColor = ClodyTheme.colors.gray01, ) @@ -339,3 +261,36 @@ private fun ShowToastMessages( ) } } + +@Composable +@Preview +private fun WriteDiaryScreenPreview() { + CLODYTheme { + WriteDiaryScreen( + isLoading = false, + entries = listOf("Entry 1", "Entry 2", "Entry 3"), + showWarnings = listOf(false, true, false), + showLimitMessage = false, + showEmptyFieldsMessage = false, + showDeleteBottomSheet = false, + entryToDelete = -1, + showDialog = false, + allFieldsEmpty = false, + onClickBack = {}, + onClickAdd = {}, + onClickRemove = {}, + onConfirmDelete = {}, + onDismissDelete = {}, + onTextChange = { _, _ -> }, + onClickComplete = {}, + onConfirmDialog = {}, + onDismissDialog = {}, + onDismissLimitMessage = {}, + onDismissEmptyFieldsMessage = {}, + year = 2023, + month = 10, + day = 5, + ) + } +} + From f51153b9fc0633ea0da42b952610298ab925dc1c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 21:11:44 +0900 Subject: [PATCH 068/299] =?UTF-8?q?[ADD/#263]=20String=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54533794..aa528824 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -63,10 +63,16 @@ 확인 - 일기를 저장할까요? - 저장한 일기는 수정이 어려워요. - 저장하기 - 아니오 + 일기를 로디에게 보낼까요? + 보낸 일기는 수정이 어려워요. + 보내기 + 취소 + + + 지금까지 쓴 일기를 임시저장할까요? + 나가기를 누르면 작성 중인 내용이 모두 사라져요. + 나가기 + 임시저장 저장 @@ -74,7 +80,7 @@ 최대 5개까지 작성할 수 있어요. - 모든 감사 일기 작성이 필요해요. + 빈 칸을 채워야 보낼 수 있어요. 신조어, 비속어, 이모지 작성은 불가능해요 From 09fc040df411bc8e2c638bd18f7958bc209681e6 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 21:12:05 +0900 Subject: [PATCH 069/299] =?UTF-8?q?[REFACTOR/#263]=20ClodyToastMessage=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/component/toast/ClodyToastMessage.kt | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/toast/ClodyToastMessage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/toast/ClodyToastMessage.kt index ff1fbf64..607a8a9f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/toast/ClodyToastMessage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/toast/ClodyToastMessage.kt @@ -2,13 +2,14 @@ package com.sopt.clody.presentation.ui.component.toast import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,6 +20,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.sopt.clody.ui.theme.CLODYTheme import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.coroutines.delay @@ -39,11 +41,15 @@ fun ClodyToastMessage( Box( modifier = modifier - .wrapContentHeight() - .background(color = backgroundColor, shape = RoundedCornerShape(28.dp)) - .padding(horizontal = 22.dp, vertical = 16.dp), + .height(42.dp) + .background(color = backgroundColor, shape = RoundedCornerShape(28.dp)), + contentAlignment = Alignment.Center, ) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.padding(horizontal = 18.dp), + ) { Image( painter = painterResource(id = iconResId), contentDescription = null, @@ -61,14 +67,15 @@ fun ClodyToastMessage( @Preview @Composable -fun PreviewCustomToastMessage() { - ClodyToastMessage( - message = "토스트 메시지", - iconResId = 0, - backgroundColor = Color(0xFF000000), - contentColor = Color(0xFFFFFFFF), - durationMillis = 3000, - onDismiss = {}, - modifier = Modifier, - ) +fun ClodyToastMessagePreview() { + CLODYTheme { + ClodyToastMessage( + message = "이메일 인증이 완료되었어요!", + iconResId = com.sopt.clody.R.drawable.ic_toast_error, + backgroundColor = ClodyTheme.colors.gray01, + contentColor = ClodyTheme.colors.white, + durationMillis = 3000L, + onDismiss = {}, + ) + } } From 8311f1a2c087276adfda381059468661ca3f0d0f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 21:12:30 +0900 Subject: [PATCH 070/299] =?UTF-8?q?[REFACTOR/#263]=20WriteDiaryTextField?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=A0=9C=EA=B1=B0=20=EA=B0=80=EB=8A=A5=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EB=B2=84=ED=8A=BC=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../textfield/WriteDiaryTextField.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt index 1f469b4c..e3a24cde 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt @@ -142,14 +142,18 @@ fun WriteDiaryTextField( .onFocusChanged { isFocused = it.isFocused }, ) - IconButton( - onClick = onRemove, - modifier = Modifier.size(28.dp), - ) { - Image( - painter = painterResource(id = R.drawable.ic_writediary_kebab), - contentDescription = "Remove", - ) + if (isRemovable) { + IconButton( + onClick = onRemove, + modifier = Modifier.size(28.dp), + ) { + Image( + painter = painterResource(id = R.drawable.ic_writediary_kebab), + contentDescription = "Remove", + ) + } + } else { + Spacer(modifier = Modifier.size(28.dp)) } } } From 9bd2a3e1bf22ee3869785ad7c4ca4dda95471a05 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 21:13:42 +0900 Subject: [PATCH 071/299] =?UTF-8?q?[ADD/#263]=20showExitDialog=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EB=B0=8F=20update=20=ED=95=A8=EC=88=98=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 --- .../ui/writediary/screen/WriteDiaryViewModel.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index 9fe15c3b..e8e5001e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -54,6 +54,9 @@ class WriteDiaryViewModel @Inject constructor( var showDialog by mutableStateOf(false) private set + var showExitDialog by mutableStateOf(false) + private set + fun writeDiary(year: Int, month: Int, day: Int, contents: List) { viewModelScope.launch { if (!networkUtil.isNetworkAvailable()) { @@ -162,6 +165,10 @@ class WriteDiaryViewModel @Inject constructor( entryToDelete = index } + fun updateShowExitDialog(show: Boolean) { + showExitDialog = show + } + companion object { const val MAX_ENTRIES = 5 const val ENTRY_REGEX = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣\\W]{2,50}$" From a7dabb5ca2608c61cf19d77bc0c6cfc324ee43b6 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 21:21:44 +0900 Subject: [PATCH 072/299] =?UTF-8?q?[UI/#263]=20AddDiaryEntryFAB=20Componen?= =?UTF-8?q?t=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 키보드 상태에 따라 FAB UI가 전환되도록 구현 최대 항목 수 도달 시 FAB 비활성화 및 색상 변경 처리 --- .../component/button/AddDiaryEntryFAB.kt | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt new file mode 100644 index 00000000..d976aac3 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt @@ -0,0 +1,95 @@ +package com.sopt.clody.presentation.ui.writediary.component.button + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +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.unit.dp +import com.sopt.clody.R +import com.sopt.clody.presentation.utils.extension.clickableWithoutRipple +import com.sopt.clody.ui.theme.ClodyTheme + +@Composable +fun BoxScope.AddDiaryEntryFAB( + isKeyboardVisible: Boolean, + isMaxReached: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val containerColor = if (isMaxReached) ClodyTheme.colors.gray04 else ClodyTheme.colors.gray02 + val contentColor = ClodyTheme.colors.white + + AnimatedContent( + targetState = isKeyboardVisible, + transitionSpec = { + fadeIn(tween(300)) togetherWith fadeOut(tween(300)) + }, + modifier = modifier + .align(Alignment.BottomEnd) + .imePadding() + .padding(end = 24.dp, bottom = 12.dp), + label = "FAB Animation", + ) { keyboardVisible -> + if (keyboardVisible) { + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(28.dp)) + .background(containerColor) + .clickableWithoutRipple(enabled = !isMaxReached) { + onClick() + }, + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(R.drawable.ic_writediary_add), + contentDescription = null, + tint = contentColor, + ) + } + } else { + Row( + modifier = Modifier + .height(42.dp) + .clip(RoundedCornerShape(24.dp)) + .background(containerColor) + .clickableWithoutRipple(enabled = !isMaxReached) { + onClick() + } + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(R.drawable.ic_writediary_add), + contentDescription = null, + tint = contentColor, + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = "추가하기", + color = contentColor, + style = ClodyTheme.typography.body2SemiBold, + ) + } + } + } +} From d2fb687eba750468e9f0cb917f23a7ff52a93109 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 21:22:00 +0900 Subject: [PATCH 073/299] =?UTF-8?q?[UI/#263]=20TopBar=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=A0=20"=EB=B3=B4=EB=82=B4=EA=B8=B0"=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=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 --- .../writediary/component/button/SendButton.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt new file mode 100644 index 00000000..7d69561c --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt @@ -0,0 +1,41 @@ +package com.sopt.clody.presentation.ui.writediary.component.button + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.sopt.clody.ui.theme.ClodyTheme + +@Composable +fun SendButton( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + Box( + modifier = modifier + .sizeIn(minWidth = 48.dp, minHeight = 48.dp) + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onClick, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = "보내기", + color = if (isPressed) ClodyTheme.colors.gray07 else ClodyTheme.colors.gray01, + style = ClodyTheme.typography.body2SemiBold, + ) + } +} From 07d7f8121e64b92e0fbb82e04e516403e5973251 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 21:22:11 +0900 Subject: [PATCH 074/299] =?UTF-8?q?[ADD/#263]=20WriteDiaryTopBar=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/topbar/WriteDiaryTopBar.kt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/topbar/WriteDiaryTopBar.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/topbar/WriteDiaryTopBar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/topbar/WriteDiaryTopBar.kt new file mode 100644 index 00000000..f84448ed --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/topbar/WriteDiaryTopBar.kt @@ -0,0 +1,51 @@ +package com.sopt.clody.presentation.ui.writediary.component.topbar + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.sopt.clody.R +import com.sopt.clody.presentation.ui.writediary.component.button.SendButton +import com.sopt.clody.ui.theme.ClodyTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WriteDiaryTopBar( + modifier: Modifier = Modifier, + onClickBack: () -> Unit, + onClickSend: () -> Unit, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, +) { + TopAppBar( + modifier = modifier.fillMaxWidth(), + title = {}, + navigationIcon = { + IconButton(onClick = onClickBack) { + Icon( + painter = painterResource(id = R.drawable.ic_nickname_back), + contentDescription = "뒤로가기", + tint = ClodyTheme.colors.gray01, + ) + } + }, + actions = { + SendButton( + modifier = Modifier + .padding(horizontal = 12.dp), + onClick = onClickSend, + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = ClodyTheme.colors.white, + ), + windowInsets = windowInsets, + ) +} From f014f4537af1f8ad6c8d9b0b225c9334f101db9c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 27 May 2025 21:25:34 +0900 Subject: [PATCH 075/299] =?UTF-8?q?[REFACTOR/#263]=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20Scr?= =?UTF-8?q?een=EC=97=90=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모든 다이얼로그 UI를 Route → Screen으로 이동하여 책임 분리 명확화 - WriteDiaryTopBar, AddDiaryEntryFAB 적용 - Box로 Screen content를 감싸고 FAB/Toast 등을 배치할 수 있도록 구조 개선 --- .../ui/writediary/screen/WriteDiaryScreen.kt | 262 +++++++++++------- 1 file changed, 168 insertions(+), 94 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index 50da8b1f..bd402a12 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -4,11 +4,15 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -16,15 +20,16 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.LoadingScreen @@ -32,9 +37,11 @@ import com.sopt.clody.presentation.ui.component.dialog.ClodyDialog import com.sopt.clody.presentation.ui.component.dialog.FailureDialog import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage import com.sopt.clody.presentation.ui.writediary.component.bottomsheet.DeleteWriteDiaryBottomSheet +import com.sopt.clody.presentation.ui.writediary.component.button.AddDiaryEntryFAB import com.sopt.clody.presentation.ui.writediary.component.text.DiaryTitleText import com.sopt.clody.presentation.ui.writediary.component.textfield.WriteDiaryTextField import com.sopt.clody.presentation.ui.writediary.component.tooltip.TooltipIcon +import com.sopt.clody.presentation.ui.writediary.component.topbar.WriteDiaryTopBar import com.sopt.clody.presentation.utils.extension.getDayOfWeek import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage import com.sopt.clody.ui.theme.CLODYTheme @@ -60,10 +67,7 @@ fun WriteDiaryRoute( val showDeleteBottomSheet by viewModel::showDeleteBottomSheet val entryToDelete by viewModel::entryToDelete val showDialog by viewModel::showDialog - - val allFieldsEmpty by remember(entries) { - derivedStateOf { entries.all { it.isEmpty() } } - } + val showExitDialog by viewModel::showExitDialog LaunchedEffect(writeDiaryState) { when (writeDiaryState) { @@ -81,10 +85,17 @@ fun WriteDiaryRoute( showLimitMessage = showLimitMessage, showEmptyFieldsMessage = showEmptyFieldsMessage, showDeleteBottomSheet = showDeleteBottomSheet, - entryToDelete = entryToDelete, showDialog = showDialog, - allFieldsEmpty = allFieldsEmpty, - onClickBack = navigateToPrevious, + showFailureDialog = showFailureDialog, + failureMessage = failureMessage, + showExitDialog = showExitDialog, + onClickBack = { + if (entries.any { it.isNotBlank() }) { + viewModel.updateShowExitDialog(true) + } else { + navigateToPrevious() + } + }, onClickAdd = viewModel::addEntry, onClickRemove = { index -> viewModel.setEntryToDeleteIndex(index) @@ -112,17 +123,16 @@ fun WriteDiaryRoute( onDismissDialog = { viewModel.updateShowDialog(false) }, onDismissLimitMessage = { viewModel.updateShowLimitMessage(false) }, onDismissEmptyFieldsMessage = { viewModel.updateShowEmptyFieldsMessage(false) }, + onDismissFailureDialog = { viewModel.resetFailureDialog() }, + onDismissExitDialog = { viewModel.updateShowExitDialog(false) }, + onConfirmExitDialog = { + viewModel.updateShowExitDialog(false) + navigateToPrevious() + }, year = year, month = month, day = date, ) - - if (showFailureDialog) { - FailureDialog( - message = failureMessage, - onDismiss = viewModel::resetFailureDialog, - ) - } } @Composable @@ -133,9 +143,7 @@ fun WriteDiaryScreen( showLimitMessage: Boolean, showEmptyFieldsMessage: Boolean, showDeleteBottomSheet: Boolean, - entryToDelete: Int, showDialog: Boolean, - allFieldsEmpty: Boolean, onClickBack: () -> Unit, onClickAdd: () -> Unit, onClickRemove: (Int) -> Unit, @@ -147,80 +155,138 @@ fun WriteDiaryScreen( onDismissDialog: () -> Unit, onDismissLimitMessage: (Boolean) -> Unit, onDismissEmptyFieldsMessage: (Boolean) -> Unit, + showFailureDialog: Boolean, + failureMessage: String, + showExitDialog: Boolean, + onDismissFailureDialog: () -> Unit, + onDismissExitDialog: () -> Unit, + onConfirmExitDialog: () -> Unit, year: Int, month: Int, day: Int, ) { val focusManager = LocalFocusManager.current + val density = LocalDensity.current + val imeBottom = WindowInsets.ime.getBottom(density) + val isKeyboardVisible = imeBottom > 0 Scaffold( topBar = { - + WriteDiaryTopBar( + onClickBack = onClickBack, + onClickSend = onClickComplete, + ) }, content = { innerPadding -> - Column( + Box( modifier = Modifier .fillMaxSize() .background(ClodyTheme.colors.white) - .padding(innerPadding) - .padding(horizontal = 24.dp) - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() }, - ) { focusManager.clearFocus() }, - horizontalAlignment = Alignment.CenterHorizontally, + .padding(innerPadding), ) { - Spacer(modifier = Modifier.heightForScreenPercentage(0.017f)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { focusManager.clearFocus() }, + ), + horizontalAlignment = Alignment.CenterHorizontally, ) { - DiaryTitleText( - date = stringResource(R.string.write_diary_month_and_date, month, day), - separator = " ", - day = getDayOfWeek(year, month, day), - ) - Spacer(modifier = Modifier.weight(1f)) - TooltipIcon( - tooltipsText = stringResource(id = R.string.write_diary_help_message), - ) - } - Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - itemsIndexed(entries, key = { index, _ -> index }) { index, text -> - WriteDiaryTextField( - entryNumber = index + 1, - text = text, - onTextChange = { newText -> onTextChange(index, newText) }, - onRemove = { onClickRemove(index) }, - isRemovable = entries.size > 1, - maxLength = 50, - showWarning = showWarnings[index], + Spacer(modifier = Modifier.heightForScreenPercentage(0.017f)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + DiaryTitleText( + date = stringResource(R.string.write_diary_month_and_date, month, day), + separator = " ", + day = getDayOfWeek(year, month, day), ) + Spacer(modifier = Modifier.weight(1f)) + TooltipIcon(tooltipsText = stringResource(id = R.string.write_diary_help_message)) + } + Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + itemsIndexed(entries, key = { index, _ -> index }) { index, text -> + WriteDiaryTextField( + entryNumber = index + 1, + text = text, + onTextChange = { newText -> onTextChange(index, newText) }, + onRemove = { onClickRemove(index) }, + isRemovable = entries.size > 1, + maxLength = 50, + showWarning = showWarnings[index], + ) + } } - } - if (showDeleteBottomSheet) { - DeleteWriteDiaryBottomSheet( - onDismissRequest = onDismissDelete, - onDeleteConfirm = onConfirmDelete, - ) + if (showDeleteBottomSheet) { + DeleteWriteDiaryBottomSheet( + onDismissRequest = onDismissDelete, + onDeleteConfirm = onConfirmDelete, + ) + } + + if (showDialog) { + ClodyDialog( + onDismiss = onDismissDialog, + titleMassage = stringResource(R.string.write_diary_dialog_title), + descriptionMassage = stringResource(R.string.write_diary_dialog_description), + confirmOption = stringResource(R.string.write_diary_dialog_confirm_option), + dismissOption = stringResource(R.string.write_diary_dialog_dismiss_option), + confirmAction = onConfirmDialog, + confirmButtonColor = ClodyTheme.colors.mainYellow, + confirmButtonTextColor = ClodyTheme.colors.gray01, + ) + } + + if (showFailureDialog) { + FailureDialog( + message = failureMessage, + onDismiss = onDismissFailureDialog, + ) + } + + if (showExitDialog) { + ClodyDialog( + titleMassage = stringResource(R.string.temp_save_dialog_exit_title), + descriptionMassage = stringResource(R.string.temp_save_dialog_exit_description), + confirmOption = stringResource(R.string.temp_save_dialog_exit_confirm), + dismissOption = stringResource(R.string.temp_save_dialog_exit_dismiss), + confirmAction = onConfirmExitDialog, + confirmButtonColor = ClodyTheme.colors.red, + confirmButtonTextColor = ClodyTheme.colors.white, + onDismiss = onDismissExitDialog, + ) + } } + AddDiaryEntryFAB( + isKeyboardVisible = isKeyboardVisible, + isMaxReached = entries.size >= 5, + onClick = onClickAdd, + ) - if (showDialog) { - ClodyDialog( - onDismiss = onDismissDialog, - titleMassage = stringResource(R.string.write_diary_dialog_title), - descriptionMassage = stringResource(R.string.write_diary_dialog_description), - confirmOption = stringResource(R.string.write_diary_dialog_confirm_option), - dismissOption = stringResource(R.string.write_diary_dialog_dismiss_option), - confirmAction = onConfirmDialog, - confirmButtonColor = ClodyTheme.colors.mainYellow, - confirmButtonTextColor = ClodyTheme.colors.gray01, + Box( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 12.dp) + .zIndex(1f), + contentAlignment = Alignment.Center, + ) { + ShowToastMessages( + showLimitMessage = showLimitMessage, + showEmptyFieldsMessage = showEmptyFieldsMessage, + onShowLimitMessageChange = onDismissLimitMessage, + onShowEmptyFieldsMessageChange = onDismissEmptyFieldsMessage, + modifier = Modifier.imePadding(), ) } } @@ -237,28 +303,33 @@ private fun ShowToastMessages( showEmptyFieldsMessage: Boolean, onShowLimitMessageChange: (Boolean) -> Unit, onShowEmptyFieldsMessageChange: (Boolean) -> Unit, - modifier: Modifier, + modifier: Modifier = Modifier, ) { - if (showLimitMessage) { - ClodyToastMessage( - message = stringResource(R.string.toast_limit_message), - iconResId = R.drawable.ic_toast_error, - backgroundColor = ClodyTheme.colors.gray04, - contentColor = ClodyTheme.colors.white, - durationMillis = 3000, - onDismiss = { onShowLimitMessageChange(false) }, - ) - } + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (showLimitMessage) { + ClodyToastMessage( + message = stringResource(R.string.toast_limit_message), + iconResId = R.drawable.ic_toast_error, + backgroundColor = ClodyTheme.colors.gray04, + contentColor = ClodyTheme.colors.white, + durationMillis = 3000, + onDismiss = { onShowLimitMessageChange(false) }, + ) + } - if (showEmptyFieldsMessage) { - ClodyToastMessage( - message = stringResource(R.string.toast_empty_fields_message), - iconResId = R.drawable.ic_toast_error, - backgroundColor = ClodyTheme.colors.gray04, - contentColor = ClodyTheme.colors.white, - durationMillis = 3000, - onDismiss = { onShowEmptyFieldsMessageChange(false) }, - ) + if (showEmptyFieldsMessage) { + ClodyToastMessage( + message = stringResource(R.string.toast_empty_fields_message), + iconResId = R.drawable.ic_toast_error, + backgroundColor = ClodyTheme.colors.gray04, + contentColor = ClodyTheme.colors.white, + durationMillis = 3000, + onDismiss = { onShowEmptyFieldsMessageChange(false) }, + ) + } } } @@ -273,9 +344,7 @@ private fun WriteDiaryScreenPreview() { showLimitMessage = false, showEmptyFieldsMessage = false, showDeleteBottomSheet = false, - entryToDelete = -1, showDialog = false, - allFieldsEmpty = false, onClickBack = {}, onClickAdd = {}, onClickRemove = {}, @@ -287,10 +356,15 @@ private fun WriteDiaryScreenPreview() { onDismissDialog = {}, onDismissLimitMessage = {}, onDismissEmptyFieldsMessage = {}, + showFailureDialog = false, + failureMessage = "", + showExitDialog = false, + onDismissFailureDialog = {}, + onDismissExitDialog = {}, + onConfirmExitDialog = {}, year = 2023, month = 10, day = 5, ) } } - From ccd3342fbaa6404863ca8dc5dd6ae8a53197f6db Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 28 May 2025 17:55:21 +0900 Subject: [PATCH 076/299] trigger CI From ac1899a26ace3efda95e42c1df6d1125b25c2906 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 1 Jun 2025 16:55:52 +0900 Subject: [PATCH 077/299] =?UTF-8?q?[ADD/#263]=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20name=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=EB=90=9C=20Amplitude=20=ED=8A=B8=EB=9E=98?= =?UTF-8?q?=ED=82=B9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/writediary/screen/WriteDiaryScreen.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index bd402a12..9127dc5a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -42,6 +42,8 @@ import com.sopt.clody.presentation.ui.writediary.component.text.DiaryTitleText import com.sopt.clody.presentation.ui.writediary.component.textfield.WriteDiaryTextField import com.sopt.clody.presentation.ui.writediary.component.tooltip.TooltipIcon import com.sopt.clody.presentation.ui.writediary.component.topbar.WriteDiaryTopBar +import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints +import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.getDayOfWeek import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage import com.sopt.clody.ui.theme.CLODYTheme @@ -52,8 +54,8 @@ fun WriteDiaryRoute( year: Int, month: Int, date: Int, - navigateToReplyLoading: (Int, Int, Int) -> Unit, - navigateToHome: (Int, Int) -> Unit, + navigateToReplyLoading: (year: Int, month: Int, date: Int) -> Unit, + navigateToHome: (year: Int, month: Int) -> Unit, navigateToPrevious: () -> Unit, viewModel: WriteDiaryViewModel = hiltViewModel(), ) { @@ -91,13 +93,18 @@ fun WriteDiaryRoute( showExitDialog = showExitDialog, onClickBack = { if (entries.any { it.isNotBlank() }) { + AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_BACK) viewModel.updateShowExitDialog(true) } else { navigateToPrevious() } }, - onClickAdd = viewModel::addEntry, + onClickAdd = { + AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_ADD_LIST) + viewModel.addEntry() + }, onClickRemove = { index -> + AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_DELETE_LIST) viewModel.setEntryToDeleteIndex(index) viewModel.updateShowDeleteBottomSheet(true) }, @@ -115,12 +122,16 @@ fun WriteDiaryRoute( if (entries.size > 1 && entries.any { it.isEmpty() }) { viewModel.updateShowEmptyFieldsMessage(true) } else { + AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_COMPLETE) viewModel.updateShowDialog(true) } } }, onConfirmDialog = { viewModel.writeDiary(year, month, date, entries) }, - onDismissDialog = { viewModel.updateShowDialog(false) }, + onDismissDialog = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.WRITING_DIARY_NO_COMPLETE) + viewModel.updateShowDialog(false) + }, onDismissLimitMessage = { viewModel.updateShowLimitMessage(false) }, onDismissEmptyFieldsMessage = { viewModel.updateShowEmptyFieldsMessage(false) }, onDismissFailureDialog = { viewModel.resetFailureDialog() }, From ef103cc8cf9abb612fe72ccad3fb4212c3ea24f9 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 2 Jun 2025 17:35:08 +0900 Subject: [PATCH 078/299] =?UTF-8?q?[ADD/#263]=20LazyColumn=EC=97=90=20navi?= =?UTF-8?q?gationBars=20&=20ime=20=ED=8C=A8=EB=94=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/writediary/screen/WriteDiaryScreen.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index 9127dc5a..8d3c533b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -13,7 +13,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Scaffold @@ -222,7 +225,11 @@ fun WriteDiaryScreen( } Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.navigationBars.union(WindowInsets.ime), + ), verticalArrangement = Arrangement.spacedBy(12.dp), ) { itemsIndexed(entries, key = { index, _ -> index }) { index, text -> From e9d0046833ee80ea80e061c506805e7ffc46b149 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 2 Jun 2025 17:44:43 +0900 Subject: [PATCH 079/299] [REFACTOR/#263] ktlint formatting --- .../ui/component/toast/ClodyToastMessage.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/toast/ClodyToastMessage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/toast/ClodyToastMessage.kt index 607a8a9f..01a3f04c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/toast/ClodyToastMessage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/toast/ClodyToastMessage.kt @@ -18,9 +18,9 @@ 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.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.sopt.clody.ui.theme.CLODYTheme +import com.sopt.clody.presentation.utils.base.BasePreview +import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.coroutines.delay @@ -65,10 +65,10 @@ fun ClodyToastMessage( } } -@Preview +@ClodyPreview @Composable -fun ClodyToastMessagePreview() { - CLODYTheme { +private fun ClodyToastMessagePreview() { + BasePreview { ClodyToastMessage( message = "이메일 인증이 완료되었어요!", iconResId = com.sopt.clody.R.drawable.ic_toast_error, From b2257e336e62556740dda9ef348776dc102f9f62 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 2 Jun 2025 17:51:50 +0900 Subject: [PATCH 080/299] =?UTF-8?q?[REFACTOR/#263]=20Preview=20->=20Base?= =?UTF-8?q?=20Preview=20=EC=82=AC=EC=9A=A9=ED=95=98=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 --- .../presentation/ui/writediary/screen/WriteDiaryScreen.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index 8d3c533b..ba1ff28c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel @@ -47,9 +46,10 @@ import com.sopt.clody.presentation.ui.writediary.component.tooltip.TooltipIcon import com.sopt.clody.presentation.ui.writediary.component.topbar.WriteDiaryTopBar import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.base.BasePreview +import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.getDayOfWeek import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage -import com.sopt.clody.ui.theme.CLODYTheme import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -351,10 +351,10 @@ private fun ShowToastMessages( } } +@ClodyPreview @Composable -@Preview private fun WriteDiaryScreenPreview() { - CLODYTheme { + BasePreview { WriteDiaryScreen( isLoading = false, entries = listOf("Entry 1", "Entry 2", "Entry 3"), From 3ebcec59811f57d3df999de4700cf43d5db42be8 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 2 Jun 2025 23:54:23 +0900 Subject: [PATCH 081/299] =?UTF-8?q?[REFACTOR/#266]=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=EC=9D=84=20=EC=A7=84=ED=96=89=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20-=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0=20:=20=EA=B0=81=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EC=A2=85=EB=A5=98=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=B3=84?= =?UTF-8?q?=EA=B0=9C=EC=98=80=EB=8D=98=20=ED=86=A0=EA=B8=80=20=EC=8A=A4?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=ED=99=94,=20=EC=95=8C=EB=9E=8C=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=84=A0=ED=83=9D=20=EB=B0=8F=20=ED=94=BC=EC=BB=A4?= =?UTF-8?q?=20=EB=B6=80=EB=B6=84=20=EC=9D=BC=EB=B6=80=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20-=20=EC=83=81=ED=83=9C=20=ED=98=B8=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=ED=8C=85=20:=20=EB=B7=B0=EB=AA=A8=EB=8D=B8=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=ED=95=9C=20=EB=B6=80=EB=B6=84=20=EC=A0=84=EB=B6=80=20?= =?UTF-8?q?Route=EB=A1=9C=20=EB=81=8C=EC=96=B4=EC=98=AC=EB=A0=A4=20Statele?= =?UTF-8?q?ss=20=ED=95=98=EA=B2=8C=20=ED=98=B8=EC=9D=B4=EC=8A=A4=ED=8C=85.?= =?UTF-8?q?=20=EC=B1=85=EC=9E=84=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EA=B0=80?= =?UTF-8?q?=EB=8F=85=EC=84=B1=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/DraftAlarmSwitch.kt | 66 ----- .../NotificationSettingTimePicker.kt | 11 +- ...ryAlarmSwitch.kt => NotificationSwitch.kt} | 34 +-- ...ingTime.kt => NotificationTimeSelector.kt} | 19 +- .../component/ReplyAlarmSwitch.kt | 66 ----- .../screen/DiaryAlarmChangeState.kt | 10 - .../screen/DraftAlarmChangeState.kt | 10 - .../screen/NotificationChangeState.kt | 10 + .../screen/NotificationSettingScreen.kt | 245 +++++++++--------- .../screen/NotificationSettingViewModel.kt | 138 +++++----- .../screen/ReplyAlarmChangeState.kt | 10 - .../presentation/utils/extension/StringExt.kt | 27 ++ 12 files changed, 253 insertions(+), 393 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DraftAlarmSwitch.kt rename app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/{DiaryAlarmSwitch.kt => NotificationSwitch.kt} (61%) rename app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/{NotificationSettingTime.kt => NotificationTimeSelector.kt} (77%) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/ReplyAlarmSwitch.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DiaryAlarmChangeState.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DraftAlarmChangeState.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationChangeState.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/ReplyAlarmChangeState.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DraftAlarmSwitch.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DraftAlarmSwitch.kt deleted file mode 100644 index 067d1a49..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DraftAlarmSwitch.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.sopt.clody.presentation.ui.setting.notificationsetting.component - -import android.content.Context -import androidx.compose.foundation.Image -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.material3.Switch -import androidx.compose.material3.SwitchDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import com.sopt.clody.R -import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto -import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationSettingViewModel -import com.sopt.clody.ui.theme.ClodyTheme - -@Composable -fun DraftAlarmSwitch( - notificationSettingViewModel: NotificationSettingViewModel, - context: Context, - title: String, - notificationInfo: NotificationInfoResponseDto, - checkedState: MutableState, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = title, - style = ClodyTheme.typography.body1Medium, - color = ClodyTheme.colors.gray03, - ) - Spacer(modifier = Modifier.weight(1f)) - Switch( - checked = checkedState.value, - onCheckedChange = { - checkedState.value = it - notificationSettingViewModel.changeDraftAlarm(context, notificationInfo, checkedState.value) - }, - modifier = Modifier.scale(0.8f), - thumbContent = { - Image( - painter = painterResource(id = R.drawable.ic_notification_setting_switch_thumb), - contentDescription = null, - ) - }, - colors = SwitchDefaults.colors( - checkedThumbColor = ClodyTheme.colors.white, - checkedTrackColor = ClodyTheme.colors.mainYellow, - uncheckedThumbColor = ClodyTheme.colors.white, - uncheckedTrackColor = ClodyTheme.colors.gray06, - uncheckedBorderColor = ClodyTheme.colors.gray06, - ), - ) - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt index f168b727..b7b52f80 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt @@ -28,14 +28,13 @@ import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.timepicker.ClodyPicker import com.sopt.clody.presentation.ui.component.timepicker.rememberPickerState -import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationSettingViewModel +import com.sopt.clody.presentation.utils.extension.to24HourFormat import com.sopt.clody.ui.theme.ClodyTheme @Composable fun NotificationSettingTimePicker( - notificationSettingViewModel: NotificationSettingViewModel, - onTimeSelected: (String) -> Unit, onDismissRequest: () -> Unit, + onConfirm: (String) -> Unit, ) { val amPmItems = remember { listOf("오전", "오후") } val hourItems = remember { (1..12).map { it.toString() } } @@ -138,12 +137,12 @@ fun NotificationSettingTimePicker( } ClodyButton( onClick = { - val selectedTime = notificationSettingViewModel.convertTo24HourFormat( + val selectedTime = Triple( amPmPickerState.selectedItem, hourPickerState.selectedItem, minutePickerState.selectedItem, - ) - onTimeSelected(selectedTime) + ).to24HourFormat() + onConfirm(selectedTime) }, text = stringResource(R.string.notification_setting_timepicker_confirm), enabled = true, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DiaryAlarmSwitch.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt similarity index 61% rename from app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DiaryAlarmSwitch.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt index 3c6705b5..92dbb0d2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/DiaryAlarmSwitch.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt @@ -1,53 +1,43 @@ package com.sopt.clody.presentation.ui.setting.notificationsetting.component -import android.content.Context +import androidx.annotation.StringRes import androidx.compose.foundation.Image 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.material3.Switch import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.stringResource import com.sopt.clody.R -import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto -import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationSettingViewModel import com.sopt.clody.ui.theme.ClodyTheme @Composable -fun DiaryAlarmSwitch( - notificationSettingViewModel: NotificationSettingViewModel, - context: Context, - title: String, - notificationInfo: NotificationInfoResponseDto, - checkedState: MutableState, +fun NotificationSwitch( + @StringRes title: Int, + checkedState: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, ) { Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), + modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Text( - text = title, + text = stringResource(title), style = ClodyTheme.typography.body1Medium, color = ClodyTheme.colors.gray03, ) Spacer(modifier = Modifier.weight(1f)) Switch( - checked = checkedState.value, - onCheckedChange = { - checkedState.value = it - notificationSettingViewModel.changeDiaryAlarm(context, notificationInfo, checkedState.value) - }, - modifier = Modifier.scale(0.8f), + checked = checkedState, + onCheckedChange = { onClick() }, + modifier = Modifier.scale(0.7f), thumbContent = { Image( painter = painterResource(id = R.drawable.ic_notification_setting_switch_thumb), diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTime.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationTimeSelector.kt similarity index 77% rename from app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTime.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationTimeSelector.kt index bab0901b..acaaf484 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTime.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationTimeSelector.kt @@ -6,8 +6,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource 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.wrapContentHeight import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -15,20 +13,17 @@ 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.unit.dp import com.sopt.clody.R import com.sopt.clody.ui.theme.ClodyTheme @Composable -fun NotificationSettingTime( - selectedTime: String, - updateNotificationTimePicker: (Boolean) -> Unit, +fun NotificationTimeSelector( + time: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, ) { Row( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(horizontal = 24.dp), + modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Text( @@ -39,14 +34,14 @@ fun NotificationSettingTime( Spacer(modifier = Modifier.weight(1f)) Row( modifier = Modifier.clickable( - onClick = { updateNotificationTimePicker(true) }, + onClick = onClick, indication = null, interactionSource = remember { MutableInteractionSource() }, ), verticalAlignment = Alignment.CenterVertically, ) { Text( - text = selectedTime, + text = time, style = ClodyTheme.typography.body3Medium, color = ClodyTheme.colors.gray05, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/ReplyAlarmSwitch.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/ReplyAlarmSwitch.kt deleted file mode 100644 index c8f18fd1..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/ReplyAlarmSwitch.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.sopt.clody.presentation.ui.setting.notificationsetting.component - -import android.content.Context -import androidx.compose.foundation.Image -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.material3.Switch -import androidx.compose.material3.SwitchDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import com.sopt.clody.R -import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto -import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationSettingViewModel -import com.sopt.clody.ui.theme.ClodyTheme - -@Composable -fun ReplyAlarmSwitch( - notificationSettingViewModel: NotificationSettingViewModel, - context: Context, - title: String, - notificationInfo: NotificationInfoResponseDto, - checkedState: MutableState, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = title, - style = ClodyTheme.typography.body1Medium, - color = ClodyTheme.colors.gray03, - ) - Spacer(modifier = Modifier.weight(1f)) - Switch( - checked = checkedState.value, - onCheckedChange = { - checkedState.value = it - notificationSettingViewModel.changeReplyAlarm(context, notificationInfo, checkedState.value) - }, - modifier = Modifier.scale(0.8f), - thumbContent = { - Image( - painter = painterResource(id = R.drawable.ic_notification_setting_switch_thumb), - contentDescription = null, - ) - }, - colors = SwitchDefaults.colors( - checkedThumbColor = ClodyTheme.colors.white, - checkedTrackColor = ClodyTheme.colors.mainYellow, - uncheckedThumbColor = ClodyTheme.colors.white, - uncheckedTrackColor = ClodyTheme.colors.gray06, - uncheckedBorderColor = ClodyTheme.colors.gray06, - ), - ) - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DiaryAlarmChangeState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DiaryAlarmChangeState.kt deleted file mode 100644 index 7bb52cd4..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DiaryAlarmChangeState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.sopt.clody.presentation.ui.setting.notificationsetting.screen - -import com.sopt.clody.data.remote.dto.response.SendNotificationResponseDto - -sealed class DiaryAlarmChangeState { - data object Idle : DiaryAlarmChangeState() - data object Loading : DiaryAlarmChangeState() - data class Success(val data: SendNotificationResponseDto) : DiaryAlarmChangeState() - data class Failure(val errorMessage: String) : DiaryAlarmChangeState() -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DraftAlarmChangeState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DraftAlarmChangeState.kt deleted file mode 100644 index 928a5620..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/DraftAlarmChangeState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.sopt.clody.presentation.ui.setting.notificationsetting.screen - -import com.sopt.clody.data.remote.dto.response.SendNotificationResponseDto - -sealed class DraftAlarmChangeState { - data object Idle : DraftAlarmChangeState() - data object Loading : DraftAlarmChangeState() - data class Success(val data: SendNotificationResponseDto) : DraftAlarmChangeState() - data class Failure(val errorMessage: String) : DraftAlarmChangeState() -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationChangeState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationChangeState.kt new file mode 100644 index 00000000..ab19b075 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationChangeState.kt @@ -0,0 +1,10 @@ +package com.sopt.clody.presentation.ui.setting.notificationsetting.screen + +import com.sopt.clody.data.remote.dto.response.SendNotificationResponseDto + +sealed class NotificationChangeState { + data object Idle : NotificationChangeState() + data object Loading : NotificationChangeState() + data class Success(val data: SendNotificationResponseDto) : NotificationChangeState() + data class Failure(val errorMessage: String) : NotificationChangeState() +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index 184aaba3..f99d33d5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -1,6 +1,5 @@ package com.sopt.clody.presentation.ui.setting.notificationsetting.screen -import android.content.Context import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -9,7 +8,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -19,6 +17,7 @@ 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 import com.sopt.clody.R @@ -29,149 +28,53 @@ import com.sopt.clody.presentation.ui.component.dialog.FailureDialog import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage import com.sopt.clody.presentation.ui.setting.component.SettingTopAppBar -import com.sopt.clody.presentation.ui.setting.notificationsetting.component.DiaryAlarmSwitch -import com.sopt.clody.presentation.ui.setting.notificationsetting.component.DraftAlarmSwitch -import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSettingTime import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSettingTimePicker -import com.sopt.clody.presentation.ui.setting.notificationsetting.component.ReplyAlarmSwitch +import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSwitch +import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationTimeSelector +import com.sopt.clody.presentation.utils.extension.convertTo12HourFormat import com.sopt.clody.ui.theme.ClodyTheme @Composable fun NotificationSettingRoute( - navigateToPrevious: () -> Unit, notificationSettingViewModel: NotificationSettingViewModel = hiltViewModel(), + navigateToPrevious: () -> Unit, ) { val context = LocalContext.current val notificationInfoState by notificationSettingViewModel.notificationInfoState.collectAsState() - val diaryAlarmChangeState by notificationSettingViewModel.diaryAlarmChangeState.collectAsState() - val draftAlarmChangeState by notificationSettingViewModel.draftAlarmChangeState.collectAsState() + val diaryAlarm by notificationSettingViewModel.diaryAlarm.collectAsState() + val draftAlarm by notificationSettingViewModel.draftAlarm.collectAsState() + val replyAlarm by notificationSettingViewModel.replyAlarm.collectAsState() + val notificationTime by notificationSettingViewModel.notificationTime.collectAsState() val notificationTimeChangeState by notificationSettingViewModel.notificationTimeChangeState.collectAsState() - val replyAlarmChangeState by notificationSettingViewModel.replyAlarmChangeState.collectAsState() val showFailureDialog by notificationSettingViewModel.showFailureDialog.collectAsState() val failureDialogMessage by notificationSettingViewModel.failureDialogMessage.collectAsState() var showNotificationTimePicker by remember { mutableStateOf(false) } var notificationInfo by remember { mutableStateOf(null) } - LaunchedEffect(Unit) { - notificationSettingViewModel.getNotificationInfo() - } - - LaunchedEffect(diaryAlarmChangeState, draftAlarmChangeState, notificationTimeChangeState, replyAlarmChangeState) { - val isSuccess = diaryAlarmChangeState is DiaryAlarmChangeState.Success || - draftAlarmChangeState is DraftAlarmChangeState.Success || - notificationTimeChangeState is NotificationTimeChangeState.Success || - replyAlarmChangeState is ReplyAlarmChangeState.Success - - if (isSuccess) { - notificationSettingViewModel.getNotificationInfo() - } - } - NotificationSettingScreen( - notificationSettingViewModel = notificationSettingViewModel, - context = context, notificationInfoState = notificationInfoState, - notificationInfo = notificationInfo, - notificationTimeChangeState = notificationTimeChangeState, - showNotificationTimePicker = showNotificationTimePicker, - updateNotificationTimePicker = { state -> showNotificationTimePicker = state }, - showFailureDialog = showFailureDialog, - failureDialogMessage = failureDialogMessage, + diaryAlarm = diaryAlarm, + draftAlarm = draftAlarm, + replyAlarm = replyAlarm, + notificationTime = notificationTime, onClickBack = navigateToPrevious, - onNotificationInfoAvailable = { notificationInfo = it }, + onClickDiarySwitch = { notificationSettingViewModel.changeDiaryAlarm(context) }, + onClickDraftSwitch = { notificationSettingViewModel.changeDraftAlarm(context) }, + onClickNotificationTime = { showNotificationTimePicker = true }, + onClickReplySwitch = { notificationSettingViewModel.changeReplyAlarm(context) }, + onClickRetry = { notificationSettingViewModel.getNotificationInfo() }, ) -} - -@Composable -fun NotificationSettingScreen( - notificationSettingViewModel: NotificationSettingViewModel, - context: Context, - notificationInfoState: NotificationInfoState, - notificationInfo: NotificationInfoResponseDto?, - notificationTimeChangeState: NotificationTimeChangeState, - showNotificationTimePicker: Boolean, - updateNotificationTimePicker: (Boolean) -> Unit, - showFailureDialog: Boolean, - failureDialogMessage: String, - onClickBack: () -> Unit, - onNotificationInfoAvailable: (NotificationInfoResponseDto) -> Unit, -) { - Scaffold( - topBar = { - SettingTopAppBar( - title = stringResource(R.string.notification_setting_title), - onClickBack = onClickBack, - ) - }, - containerColor = ClodyTheme.colors.white, - ) { innerPadding -> - when (notificationInfoState) { - is NotificationInfoState.Idle -> {} - - is NotificationInfoState.Loading -> { - LoadingScreen() - } - - is NotificationInfoState.Success -> { - val notificationInfo = notificationInfoState.data - onNotificationInfoAvailable(notificationInfo) - val notificationTime = notificationSettingViewModel.convertTo12HourFormat(notificationInfo.time) - Column( - modifier = Modifier - .padding(innerPadding), - ) { - Spacer(modifier = Modifier.height(20.dp)) - DiaryAlarmSwitch( - notificationSettingViewModel = notificationSettingViewModel, - context = context, - title = stringResource(R.string.notification_setting_write_diary), - notificationInfo = notificationInfo, - checkedState = remember { mutableStateOf(notificationInfo.isDiaryAlarm) }, - ) - Spacer(modifier = Modifier.height(32.dp)) - DraftAlarmSwitch( - notificationSettingViewModel = notificationSettingViewModel, - context = context, - title = stringResource(R.string.notification_setting_draft_diary), - notificationInfo = notificationInfo, - checkedState = remember { mutableStateOf(notificationInfo.isDraftAlarm) }, - ) - Spacer(modifier = Modifier.height(32.dp)) - NotificationSettingTime( - selectedTime = notificationTime, - updateNotificationTimePicker = updateNotificationTimePicker, - ) - Spacer(modifier = Modifier.height(32.dp)) - ReplyAlarmSwitch( - notificationSettingViewModel = notificationSettingViewModel, - context = context, - title = stringResource(R.string.notification_setting_reply_diary), - notificationInfo = notificationInfo, - checkedState = remember { mutableStateOf(notificationInfo.isReplyAlarm) }, - ) - } - } - - is NotificationInfoState.Failure -> { - FailureScreen( - message = notificationInfoState.errorMessage, - confirmAction = { notificationSettingViewModel.getNotificationInfo() }, - ) - } - } - } if (showNotificationTimePicker) { - ClodyPopupBottomSheet(onDismissRequest = { updateNotificationTimePicker(false) }) { + ClodyPopupBottomSheet(onDismissRequest = { showNotificationTimePicker = false }) { NotificationSettingTimePicker( - notificationSettingViewModel = notificationSettingViewModel, - onTimeSelected = { newNotificationTime -> + onDismissRequest = { showNotificationTimePicker = false }, + onConfirm = { newNotificationTime -> notificationInfo?.let { - notificationSettingViewModel.changeNotificationTime(context, it, newNotificationTime) + notificationSettingViewModel.changeNotificationTime(context, newNotificationTime) } - updateNotificationTimePicker(false) + showNotificationTimePicker = false }, - onDismissRequest = { updateNotificationTimePicker(false) }, ) } } @@ -189,7 +92,7 @@ fun NotificationSettingScreen( backgroundColor = ClodyTheme.colors.gray04, contentColor = ClodyTheme.colors.white, durationMillis = 3000, - onDismiss = { notificationSettingViewModel.resetNotificationChangeState() }, + onDismiss = { notificationSettingViewModel.resetNotificationTimeChangeState() }, ) } } @@ -201,3 +104,103 @@ fun NotificationSettingScreen( ) } } + +@Composable +fun NotificationSettingScreen( + notificationInfoState: NotificationInfoState, + diaryAlarm: Boolean, + draftAlarm: Boolean, + replyAlarm: Boolean, + notificationTime: String, + onClickBack: () -> Unit, + onClickDiarySwitch: () -> Unit, + onClickDraftSwitch: () -> Unit, + onClickNotificationTime: () -> Unit, + onClickReplySwitch: () -> Unit, + onClickRetry: () -> Unit, +) { + Scaffold( + topBar = { + SettingTopAppBar( + title = stringResource(R.string.notification_setting_title), + onClickBack = onClickBack, + ) + }, + containerColor = ClodyTheme.colors.white, + content = { innerPadding -> + when (notificationInfoState) { + is NotificationInfoState.Idle -> {} + + is NotificationInfoState.Loading -> { + LoadingScreen() + } + + is NotificationInfoState.Success -> { + Column( + modifier = Modifier + .padding(innerPadding) + .padding(horizontal = 24.dp), + ) { + Spacer(modifier = Modifier.height(24.dp)) + NotificationSwitch( + title = R.string.notification_setting_write_diary, + checkedState = diaryAlarm, + onClick = onClickDiarySwitch, + ) + Spacer(modifier = Modifier.height(12.dp)) + NotificationSwitch( + title = R.string.notification_setting_draft_diary, + checkedState = draftAlarm, + onClick = onClickDraftSwitch, + ) + Spacer(modifier = Modifier.height(24.dp)) + NotificationTimeSelector( + time = notificationTime.convertTo12HourFormat(), + onClick = onClickNotificationTime, + ) + Spacer(modifier = Modifier.height(24.dp)) + NotificationSwitch( + title = R.string.notification_setting_reply_diary, + checkedState = replyAlarm, + onClick = onClickReplySwitch, + ) + } + } + + is NotificationInfoState.Failure -> { + FailureScreen( + message = notificationInfoState.errorMessage, + confirmAction = onClickRetry, + ) + } + } + }, + ) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewNotificationSettingScreen() { + ClodyTheme { + NotificationSettingScreen( + notificationInfoState = NotificationInfoState.Success( + NotificationInfoResponseDto( + isDiaryAlarm = true, + isDraftAlarm = false, + isReplyAlarm = true, + time = "21:30", + ), + ), + diaryAlarm = true, + draftAlarm = false, + replyAlarm = true, + notificationTime = "21:30", + onClickBack = {}, + onClickDiarySwitch = {}, + onClickDraftSwitch = {}, + onClickNotificationTime = {}, + onClickReplySwitch = {}, + onClickRetry = {}, + ) + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt index 0bf7a3d1..834357d3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto -import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE @@ -22,21 +21,27 @@ class NotificationSettingViewModel @Inject constructor( private val networkUtil: NetworkUtil, ) : ViewModel() { + private val _diaryAlarm = MutableStateFlow(false) + val diaryAlarm: StateFlow = _diaryAlarm + + private val _draftAlarm = MutableStateFlow(false) + val draftAlarm: StateFlow = _draftAlarm + + private val _replyAlarm = MutableStateFlow(false) + val replyAlarm: StateFlow = _replyAlarm + + private val _notificationTime = MutableStateFlow("") + val notificationTime: StateFlow = _notificationTime + private val _notificationInfoState = MutableStateFlow(NotificationInfoState.Idle) val notificationInfoState: StateFlow = _notificationInfoState - private val _diaryAlarmChangeState = MutableStateFlow(DiaryAlarmChangeState.Idle) - val diaryAlarmChangeState: StateFlow = _diaryAlarmChangeState - - private val _draftAlarmChangeState = MutableStateFlow(DraftAlarmChangeState.Idle) - val draftAlarmChangeState: StateFlow = _draftAlarmChangeState + private val _notificationChangeState = MutableStateFlow(NotificationChangeState.Idle) + val notificationChangeState: StateFlow = _notificationChangeState private val _notificationTimeChangeState = MutableStateFlow(NotificationTimeChangeState.Idle) val notificationTimeChangeState: StateFlow = _notificationTimeChangeState - private val _replyAlarmChangeState = MutableStateFlow(ReplyAlarmChangeState.Idle) - val replyAlarmChangeState: StateFlow = _replyAlarmChangeState - private val _showFailureDialog = MutableStateFlow(false) val showFailureDialog: StateFlow = _showFailureDialog @@ -46,6 +51,10 @@ class NotificationSettingViewModel @Inject constructor( private val maxRetryCount = 3 private var retryCount = 0 + init { + getNotificationInfo() + } + fun getNotificationInfo() { if (retryCount >= maxRetryCount) return _notificationInfoState.value = NotificationInfoState.Loading @@ -58,6 +67,10 @@ class NotificationSettingViewModel @Inject constructor( _notificationInfoState.value = result.fold( onSuccess = { retryCount = 0 + _diaryAlarm.value = it.isDiaryAlarm + _draftAlarm.value = it.isDraftAlarm + _replyAlarm.value = it.isReplyAlarm + _notificationTime.value = it.time NotificationInfoState.Success(it) }, onFailure = { @@ -76,8 +89,8 @@ class NotificationSettingViewModel @Inject constructor( } } - fun changeDiaryAlarm(context: Context, notificationInfo: NotificationInfoResponseDto, diaryAlarm: Boolean) { - _diaryAlarmChangeState.value = DiaryAlarmChangeState.Loading + fun changeDiaryAlarm(context: Context) { + _notificationChangeState.value = NotificationChangeState.Loading viewModelScope.launch { if (!networkUtil.isNetworkAvailable()) { _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE @@ -86,18 +99,21 @@ class NotificationSettingViewModel @Inject constructor( } val fcmToken = getTokenFromPreferences(context) if (fcmToken.isNullOrBlank()) { - _diaryAlarmChangeState.value = DiaryAlarmChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") + _notificationChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") return@launch } val requestDto = SendNotificationRequestDto( - isDiaryAlarm = diaryAlarm, - isDraftAlarm = notificationInfo.isDraftAlarm, - isReplyAlarm = notificationInfo.isReplyAlarm, - time = notificationInfo.time, + isDiaryAlarm = !_diaryAlarm.value, + isDraftAlarm = _draftAlarm.value, + isReplyAlarm = _replyAlarm.value, + time = _notificationTime.value, fcmToken = fcmToken, ) notificationRepository.sendNotification(requestDto).fold( - onSuccess = { _diaryAlarmChangeState.value = DiaryAlarmChangeState.Success(it) }, + onSuccess = { + _notificationChangeState.value = NotificationChangeState.Success(it) + getNotificationInfo() + }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { FAILURE_TEMPORARY_MESSAGE @@ -105,14 +121,14 @@ class NotificationSettingViewModel @Inject constructor( UNKNOWN_ERROR } _showFailureDialog.value = true - DiaryAlarmChangeState.Failure(_failureDialogMessage.value) + _notificationChangeState.value = NotificationChangeState.Failure(_failureDialogMessage.value) }, ) } } - fun changeDraftAlarm(context: Context, notificationInfo: NotificationInfoResponseDto, draftAlarm: Boolean) { - _notificationTimeChangeState.value = NotificationTimeChangeState.Loading + fun changeDraftAlarm(context: Context) { + _notificationChangeState.value = NotificationChangeState.Loading viewModelScope.launch { if (!networkUtil.isNetworkAvailable()) { _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE @@ -121,18 +137,21 @@ class NotificationSettingViewModel @Inject constructor( } val fcmToken = getTokenFromPreferences(context) if (fcmToken.isNullOrBlank()) { - _diaryAlarmChangeState.value = DiaryAlarmChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") + _notificationChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") return@launch } val requestDto = SendNotificationRequestDto( - isDiaryAlarm = notificationInfo.isDiaryAlarm, - isDraftAlarm = draftAlarm, - isReplyAlarm = notificationInfo.isReplyAlarm, - time = notificationInfo.time, + isDiaryAlarm = _diaryAlarm.value, + isDraftAlarm = !_draftAlarm.value, + isReplyAlarm = _replyAlarm.value, + time = _notificationTime.value, fcmToken = fcmToken, ) notificationRepository.sendNotification(requestDto).fold( - onSuccess = { _diaryAlarmChangeState.value = DiaryAlarmChangeState.Success(it) }, + onSuccess = { + _notificationChangeState.value = NotificationChangeState.Success(it) + getNotificationInfo() + }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { FAILURE_TEMPORARY_MESSAGE @@ -140,13 +159,13 @@ class NotificationSettingViewModel @Inject constructor( UNKNOWN_ERROR } _showFailureDialog.value = true - DiaryAlarmChangeState.Failure(_failureDialogMessage.value) + _notificationChangeState.value = NotificationChangeState.Failure(_failureDialogMessage.value) }, ) } } - fun changeNotificationTime(context: Context, notificationInfo: NotificationInfoResponseDto, time: String) { + fun changeNotificationTime(context: Context, time: String) { _notificationTimeChangeState.value = NotificationTimeChangeState.Loading viewModelScope.launch { if (!networkUtil.isNetworkAvailable()) { @@ -160,14 +179,17 @@ class NotificationSettingViewModel @Inject constructor( return@launch } val requestDto = SendNotificationRequestDto( - isDiaryAlarm = notificationInfo.isDiaryAlarm, - isDraftAlarm = notificationInfo.isDraftAlarm, - isReplyAlarm = notificationInfo.isReplyAlarm, + isDiaryAlarm = _diaryAlarm.value, + isDraftAlarm = _draftAlarm.value, + isReplyAlarm = _replyAlarm.value, time = time, fcmToken = fcmToken, ) notificationRepository.sendNotification(requestDto).fold( - onSuccess = { _notificationTimeChangeState.value = NotificationTimeChangeState.Success(it) }, + onSuccess = { + _notificationTimeChangeState.value = NotificationTimeChangeState.Success(it) + getNotificationInfo() + }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { FAILURE_TEMPORARY_MESSAGE @@ -175,14 +197,14 @@ class NotificationSettingViewModel @Inject constructor( UNKNOWN_ERROR } _showFailureDialog.value = true - NotificationInfoState.Failure(_failureDialogMessage.value) + _notificationTimeChangeState.value = NotificationTimeChangeState.Failure(_failureDialogMessage.value) }, ) } } - fun changeReplyAlarm(context: Context, notificationInfo: NotificationInfoResponseDto, replyAlarm: Boolean) { - _replyAlarmChangeState.value = ReplyAlarmChangeState.Loading + fun changeReplyAlarm(context: Context) { + _notificationChangeState.value = NotificationChangeState.Loading viewModelScope.launch { if (!networkUtil.isNetworkAvailable()) { _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE @@ -191,18 +213,21 @@ class NotificationSettingViewModel @Inject constructor( } val fcmToken = getTokenFromPreferences(context) if (fcmToken.isNullOrBlank()) { - _replyAlarmChangeState.value = ReplyAlarmChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") + _notificationChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") return@launch } val requestDto = SendNotificationRequestDto( - isDiaryAlarm = notificationInfo.isDiaryAlarm, - isDraftAlarm = notificationInfo.isDraftAlarm, - isReplyAlarm = replyAlarm, - time = notificationInfo.time, + isDiaryAlarm = _diaryAlarm.value, + isDraftAlarm = _draftAlarm.value, + isReplyAlarm = !_replyAlarm.value, + time = _notificationTime.value, fcmToken = fcmToken, ) notificationRepository.sendNotification(requestDto).fold( - onSuccess = { _replyAlarmChangeState.value = ReplyAlarmChangeState.Success(it) }, + onSuccess = { + _notificationChangeState.value = NotificationChangeState.Success(it) + getNotificationInfo() + }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { FAILURE_TEMPORARY_MESSAGE @@ -210,13 +235,13 @@ class NotificationSettingViewModel @Inject constructor( UNKNOWN_ERROR } _showFailureDialog.value = true - ReplyAlarmChangeState.Failure(_failureDialogMessage.value) + _notificationChangeState.value = NotificationChangeState.Failure(_failureDialogMessage.value) }, ) } } - fun resetNotificationChangeState() { + fun resetNotificationTimeChangeState() { _notificationTimeChangeState.value = NotificationTimeChangeState.Idle } @@ -229,31 +254,4 @@ class NotificationSettingViewModel @Inject constructor( val sharedPreferences = context.getSharedPreferences("fcm_prefs", Context.MODE_PRIVATE) return sharedPreferences.getString("fcm_token", null) } - - fun convertTo12HourFormat(time: String): String { - val (hourBefore, minuteBefore) = time.split(":").map { it.toInt() } - - val amPm = if (hourBefore < 12) "오전" else "오후" - - val hourAfter = when { - hourBefore == 0 -> 12 - hourBefore > 12 -> hourBefore - 12 - else -> hourBefore - } - - val minuteAfter = if (minuteBefore == 0) "00" else minuteBefore - - return String.format("$amPm ${hourAfter}시 ${minuteAfter}분") - } - - fun convertTo24HourFormat(amPm: String, hour: String, minute: String): String { - val hourInt = if (amPm == "오후" && hour.toInt() != 12) { - hour.toInt() + 12 - } else if (amPm == "오전" && hour.toInt() == 12) { - 0 - } else { - hour.toInt() - } - return String.format("%02d:%02d", hourInt, minute.toInt()) - } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/ReplyAlarmChangeState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/ReplyAlarmChangeState.kt deleted file mode 100644 index f11f7372..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/ReplyAlarmChangeState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.sopt.clody.presentation.ui.setting.notificationsetting.screen - -import com.sopt.clody.data.remote.dto.response.SendNotificationResponseDto - -sealed class ReplyAlarmChangeState { - data object Idle : ReplyAlarmChangeState() - data object Loading : ReplyAlarmChangeState() - data class Success(val data: SendNotificationResponseDto) : ReplyAlarmChangeState() - data class Failure(val errorMessage: String) : ReplyAlarmChangeState() -} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt new file mode 100644 index 00000000..6eeb8e26 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt @@ -0,0 +1,27 @@ +package com.sopt.clody.presentation.utils.extension + +fun String.convertTo12HourFormat(): String { + val (hourBefore, minuteBefore) = this.split(":").map { it.toInt() } + + val amPm = if (hourBefore < 12) "오전" else "오후" + + val hourAfter = when { + hourBefore == 0 -> 12 + hourBefore > 12 -> hourBefore - 12 + else -> hourBefore + } + + val minuteAfter = if (minuteBefore == 0) "00" else minuteBefore + + return String.format("$amPm ${hourAfter}시 ${minuteAfter}분") +} + +fun Triple.to24HourFormat(): String { + val (amPm, hour, minute) = this + val hourInt = when { + amPm == "오후" && hour.toInt() != 12 -> hour.toInt() + 12 + amPm == "오전" && hour.toInt() == 12 -> 0 + else -> hour.toInt() + } + return String.format("%02d:%02d", hourInt, minute.toInt()) +} From 0f7fcef72634230819cf104159f1dfa57179f183 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 3 Jun 2025 01:35:43 +0900 Subject: [PATCH 082/299] =?UTF-8?q?[CHORE/#266]=20Dead=20Code=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notificationsetting/screen/NotificationSettingScreen.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index f99d33d5..3e4e037c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -70,9 +70,7 @@ fun NotificationSettingRoute( NotificationSettingTimePicker( onDismissRequest = { showNotificationTimePicker = false }, onConfirm = { newNotificationTime -> - notificationInfo?.let { - notificationSettingViewModel.changeNotificationTime(context, newNotificationTime) - } + notificationSettingViewModel.changeNotificationTime(context, newNotificationTime) showNotificationTimePicker = false }, ) From 3b819a99a74bf283514dc22ed900b7bfa4e20a12 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 3 Jun 2025 01:36:07 +0900 Subject: [PATCH 083/299] =?UTF-8?q?[CHORE/#266]=20ClodyPreview=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notificationsetting/screen/NotificationSettingScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index 3e4e037c..948f0507 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -17,7 +17,6 @@ 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 import com.sopt.clody.R @@ -31,6 +30,7 @@ import com.sopt.clody.presentation.ui.setting.component.SettingTopAppBar import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSettingTimePicker import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSwitch import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationTimeSelector +import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.convertTo12HourFormat import com.sopt.clody.ui.theme.ClodyTheme @@ -176,7 +176,7 @@ fun NotificationSettingScreen( ) } -@Preview(showBackground = true) +@ClodyPreview @Composable private fun PreviewNotificationSettingScreen() { ClodyTheme { From a77e122e5da1e26697df48a158f3c61803f99c82 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 3 Jun 2025 01:37:11 +0900 Subject: [PATCH 084/299] =?UTF-8?q?[CHORE/#266]=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notificationsetting/component/NotificationSwitch.kt | 4 ++-- .../notificationsetting/screen/NotificationSettingScreen.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt index 92dbb0d2..3464b406 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSwitch.kt @@ -21,7 +21,7 @@ import com.sopt.clody.ui.theme.ClodyTheme fun NotificationSwitch( @StringRes title: Int, checkedState: Boolean, - onClick: () -> Unit, + onCheckedChanged: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -36,7 +36,7 @@ fun NotificationSwitch( Spacer(modifier = Modifier.weight(1f)) Switch( checked = checkedState, - onCheckedChange = { onClick() }, + onCheckedChange = onCheckedChanged, modifier = Modifier.scale(0.7f), thumbContent = { Image( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index 948f0507..9f83724c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -111,10 +111,10 @@ fun NotificationSettingScreen( replyAlarm: Boolean, notificationTime: String, onClickBack: () -> Unit, - onClickDiarySwitch: () -> Unit, - onClickDraftSwitch: () -> Unit, + onClickDiarySwitch: (Boolean) -> Unit, + onClickDraftSwitch: (Boolean) -> Unit, onClickNotificationTime: () -> Unit, - onClickReplySwitch: () -> Unit, + onClickReplySwitch: (Boolean) -> Unit, onClickRetry: () -> Unit, ) { Scaffold( From 6001a36704253bdfa909355e1b90cd403196cb4b Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 3 Jun 2025 01:37:35 +0900 Subject: [PATCH 085/299] =?UTF-8?q?[REFACTOR/#266]=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20=EB=8B=A8=EC=9D=BC=ED=99=94=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/clody/domain/Notification.kt | 5 + .../screen/NotificationSettingScreen.kt | 14 +-- .../screen/NotificationSettingViewModel.kt | 93 +++---------------- 3 files changed, 27 insertions(+), 85 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/domain/Notification.kt diff --git a/app/src/main/java/com/sopt/clody/domain/Notification.kt b/app/src/main/java/com/sopt/clody/domain/Notification.kt new file mode 100644 index 00000000..5135ab84 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/Notification.kt @@ -0,0 +1,5 @@ +package com.sopt.clody.domain + +enum class Notification { + DIARY, DRAFT, REPLY +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index 9f83724c..2fca35b5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto +import com.sopt.clody.domain.Notification import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.dialog.FailureDialog @@ -49,7 +50,6 @@ fun NotificationSettingRoute( val showFailureDialog by notificationSettingViewModel.showFailureDialog.collectAsState() val failureDialogMessage by notificationSettingViewModel.failureDialogMessage.collectAsState() var showNotificationTimePicker by remember { mutableStateOf(false) } - var notificationInfo by remember { mutableStateOf(null) } NotificationSettingScreen( notificationInfoState = notificationInfoState, @@ -58,10 +58,10 @@ fun NotificationSettingRoute( replyAlarm = replyAlarm, notificationTime = notificationTime, onClickBack = navigateToPrevious, - onClickDiarySwitch = { notificationSettingViewModel.changeDiaryAlarm(context) }, - onClickDraftSwitch = { notificationSettingViewModel.changeDraftAlarm(context) }, + onClickDiarySwitch = { notificationSettingViewModel.changeAlarm(context, Notification.DIARY) }, + onClickDraftSwitch = { notificationSettingViewModel.changeAlarm(context, Notification.DRAFT) }, onClickNotificationTime = { showNotificationTimePicker = true }, - onClickReplySwitch = { notificationSettingViewModel.changeReplyAlarm(context) }, + onClickReplySwitch = { notificationSettingViewModel.changeAlarm(context, Notification.REPLY) }, onClickRetry = { notificationSettingViewModel.getNotificationInfo() }, ) @@ -143,13 +143,13 @@ fun NotificationSettingScreen( NotificationSwitch( title = R.string.notification_setting_write_diary, checkedState = diaryAlarm, - onClick = onClickDiarySwitch, + onCheckedChanged = onClickDiarySwitch, ) Spacer(modifier = Modifier.height(12.dp)) NotificationSwitch( title = R.string.notification_setting_draft_diary, checkedState = draftAlarm, - onClick = onClickDraftSwitch, + onCheckedChanged = onClickDraftSwitch, ) Spacer(modifier = Modifier.height(24.dp)) NotificationTimeSelector( @@ -160,7 +160,7 @@ fun NotificationSettingScreen( NotificationSwitch( title = R.string.notification_setting_reply_diary, checkedState = replyAlarm, - onClick = onClickReplySwitch, + onCheckedChanged = onClickReplySwitch, ) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt index 834357d3..20a90efc 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.domain.Notification import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE @@ -89,7 +90,7 @@ class NotificationSettingViewModel @Inject constructor( } } - fun changeDiaryAlarm(context: Context) { + fun changeAlarm(context: Context, notificationType: Notification) { _notificationChangeState.value = NotificationChangeState.Loading viewModelScope.launch { if (!networkUtil.isNetworkAvailable()) { @@ -97,56 +98,30 @@ class NotificationSettingViewModel @Inject constructor( _showFailureDialog.value = true return@launch } + val fcmToken = getTokenFromPreferences(context) if (fcmToken.isNullOrBlank()) { _notificationChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") return@launch } + val requestDto = SendNotificationRequestDto( - isDiaryAlarm = !_diaryAlarm.value, - isDraftAlarm = _draftAlarm.value, - isReplyAlarm = _replyAlarm.value, - time = _notificationTime.value, - fcmToken = fcmToken, - ) - notificationRepository.sendNotification(requestDto).fold( - onSuccess = { - _notificationChangeState.value = NotificationChangeState.Success(it) - getNotificationInfo() + isDiaryAlarm = when (notificationType) { + Notification.DIARY -> !_diaryAlarm.value + else -> _diaryAlarm.value }, - onFailure = { - _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE - } else { - UNKNOWN_ERROR - } - _showFailureDialog.value = true - _notificationChangeState.value = NotificationChangeState.Failure(_failureDialogMessage.value) + isDraftAlarm = when (notificationType) { + Notification.DRAFT -> !_draftAlarm.value + else -> _draftAlarm.value + }, + isReplyAlarm = when (notificationType) { + Notification.REPLY -> !_replyAlarm.value + else -> _replyAlarm.value }, - ) - } - } - - fun changeDraftAlarm(context: Context) { - _notificationChangeState.value = NotificationChangeState.Loading - viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE - _showFailureDialog.value = true - return@launch - } - val fcmToken = getTokenFromPreferences(context) - if (fcmToken.isNullOrBlank()) { - _notificationChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") - return@launch - } - val requestDto = SendNotificationRequestDto( - isDiaryAlarm = _diaryAlarm.value, - isDraftAlarm = !_draftAlarm.value, - isReplyAlarm = _replyAlarm.value, time = _notificationTime.value, fcmToken = fcmToken, ) + notificationRepository.sendNotification(requestDto).fold( onSuccess = { _notificationChangeState.value = NotificationChangeState.Success(it) @@ -203,44 +178,6 @@ class NotificationSettingViewModel @Inject constructor( } } - fun changeReplyAlarm(context: Context) { - _notificationChangeState.value = NotificationChangeState.Loading - viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE - _showFailureDialog.value = true - return@launch - } - val fcmToken = getTokenFromPreferences(context) - if (fcmToken.isNullOrBlank()) { - _notificationChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") - return@launch - } - val requestDto = SendNotificationRequestDto( - isDiaryAlarm = _diaryAlarm.value, - isDraftAlarm = _draftAlarm.value, - isReplyAlarm = !_replyAlarm.value, - time = _notificationTime.value, - fcmToken = fcmToken, - ) - notificationRepository.sendNotification(requestDto).fold( - onSuccess = { - _notificationChangeState.value = NotificationChangeState.Success(it) - getNotificationInfo() - }, - onFailure = { - _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE - } else { - UNKNOWN_ERROR - } - _showFailureDialog.value = true - _notificationChangeState.value = NotificationChangeState.Failure(_failureDialogMessage.value) - }, - ) - } - } - fun resetNotificationTimeChangeState() { _notificationTimeChangeState.value = NotificationTimeChangeState.Idle } From 2ec144a43410a3cdd7803468a6c7be4b0c0a86bc Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 4 Jun 2025 16:09:51 +0900 Subject: [PATCH 086/299] =?UTF-8?q?[FEAT/#268]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=B5=9C=EC=B4=88=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=EB=A5=BC=20=ED=8C=90=EB=8B=A8=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=9C=20flag=EB=A5=BC=20=EB=8B=B4?= =?UTF-8?q?=EC=9D=84=20SharedPreferences=EB=A5=BC=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/datastore/TokenDataStoreImpl.kt | 3 ++- .../datasource/FirstDraftLocalDataSource.kt | 11 +++++++++ .../FirstDraftLocalDataSourceImpl.kt | 23 +++++++++++++++++++ .../sopt/clody/di/SharedPreferencesModule.kt | 18 +++++++++++++++ .../com/sopt/clody/di/TokenDataStoreModule.kt | 3 ++- .../com/sopt/clody/di/qualifier/Qualifier.kt | 11 +++++++++ 6 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/data/local/datasource/FirstDraftLocalDataSource.kt create mode 100644 app/src/main/java/com/sopt/clody/data/local/datasourceimpl/FirstDraftLocalDataSourceImpl.kt create mode 100644 app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt diff --git a/app/src/main/java/com/sopt/clody/data/datastore/TokenDataStoreImpl.kt b/app/src/main/java/com/sopt/clody/data/datastore/TokenDataStoreImpl.kt index c71dda3f..04f62ef6 100644 --- a/app/src/main/java/com/sopt/clody/data/datastore/TokenDataStoreImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/datastore/TokenDataStoreImpl.kt @@ -1,10 +1,11 @@ package com.sopt.clody.data.datastore import android.content.SharedPreferences +import com.sopt.clody.di.qualifier.TokenPrefs import javax.inject.Inject class TokenDataStoreImpl @Inject constructor( - private val sharedPreferences: SharedPreferences, + @TokenPrefs private val sharedPreferences: SharedPreferences, ) : TokenDataStore { override var accessToken: String get() = sharedPreferences.getString(ACCESS_TOKEN, "") ?: "" diff --git a/app/src/main/java/com/sopt/clody/data/local/datasource/FirstDraftLocalDataSource.kt b/app/src/main/java/com/sopt/clody/data/local/datasource/FirstDraftLocalDataSource.kt new file mode 100644 index 00000000..07ca4f3b --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/local/datasource/FirstDraftLocalDataSource.kt @@ -0,0 +1,11 @@ +package com.sopt.clody.data.local.datasource + +/** + * 임시 저장 최초 사용 여부 판단을 위한 SharedPreferences + * @property isDraftUsed 임시 저장 사용 여부 + * @property isFirstUse 임시 저장 최초 사용 여부 + */ +interface FirstDraftLocalDataSource { + var isDraftUsed: Boolean + var isFirstUse: Boolean +} diff --git a/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/FirstDraftLocalDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/FirstDraftLocalDataSourceImpl.kt new file mode 100644 index 00000000..c26a81d9 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/FirstDraftLocalDataSourceImpl.kt @@ -0,0 +1,23 @@ +package com.sopt.clody.data.local.datasourceimpl + +import android.content.SharedPreferences +import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource +import com.sopt.clody.di.qualifier.FirstDraftPrefs +import javax.inject.Inject + +class FirstDraftLocalDataSourceImpl @Inject constructor( + @FirstDraftPrefs private val sharedPreferences: SharedPreferences, +) : FirstDraftLocalDataSource { + override var isDraftUsed: Boolean + get() = sharedPreferences.getBoolean(IS_DRAFT_USED, false) + set(value) = sharedPreferences.edit().putBoolean(IS_DRAFT_USED, value).apply() + + override var isFirstUse: Boolean + get() = sharedPreferences.getBoolean(IS_FIRST_USE, false) + set(value) = sharedPreferences.edit().putBoolean(IS_FIRST_USE, value).apply() + + companion object { + private const val IS_DRAFT_USED = "IS_DRAFT_USED" + private const val IS_FIRST_USE = "IS_FIRST_USE" + } +} diff --git a/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt b/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt index 4ea8c970..8e11e9aa 100644 --- a/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt +++ b/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt @@ -2,6 +2,10 @@ package com.sopt.clody.di import android.content.Context import android.content.SharedPreferences +import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource +import com.sopt.clody.data.local.datasourceimpl.FirstDraftLocalDataSourceImpl +import com.sopt.clody.di.qualifier.FirstDraftPrefs +import com.sopt.clody.di.qualifier.TokenPrefs import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -12,9 +16,23 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object SharedPreferencesModule { + + @TokenPrefs @Provides @Singleton fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences { return context.getSharedPreferences("token_prefs", Context.MODE_PRIVATE) } + + @FirstDraftPrefs + @Provides + @Singleton + fun provideFirstDraftSharedPreferences(@ApplicationContext context: Context): SharedPreferences { + return context.getSharedPreferences("first_draft_prefs", Context.MODE_PRIVATE) + } + + @Provides + @Singleton + fun provideFirstDraftLocalDataSource(@FirstDraftPrefs sharedPreferences: SharedPreferences): FirstDraftLocalDataSource = + FirstDraftLocalDataSourceImpl(sharedPreferences) } diff --git a/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt b/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt index 3f1462ad..9f33f96d 100644 --- a/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt +++ b/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt @@ -3,6 +3,7 @@ package com.sopt.clody.di import android.content.SharedPreferences import com.sopt.clody.data.datastore.TokenDataStore import com.sopt.clody.data.datastore.TokenDataStoreImpl +import com.sopt.clody.di.qualifier.TokenPrefs import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,7 +15,7 @@ import javax.inject.Singleton object TokenDataStoreModule { @Provides @Singleton - fun provideTokenDataStore(sharedPreferences: SharedPreferences): TokenDataStore { + fun provideTokenDataStore(@TokenPrefs sharedPreferences: SharedPreferences): TokenDataStore { return TokenDataStoreImpl(sharedPreferences) } } diff --git a/app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt b/app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt new file mode 100644 index 00000000..dc14802f --- /dev/null +++ b/app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt @@ -0,0 +1,11 @@ +package com.sopt.clody.di.qualifier + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class TokenPrefs + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class FirstDraftPrefs From affe7c3f57345bf56c7f4be03791a66c77b12ed4 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 4 Jun 2025 16:11:17 +0900 Subject: [PATCH 087/299] =?UTF-8?q?[FEAT/#268]=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EC=96=BC=EB=A1=9C=EA=B7=B8=EC=9D=98=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EC=9D=84=20=EB=B2=84=ED=8A=BC=ED=95=98?= =?UTF-8?q?=EB=A9=B4=20flag=EB=A5=BC=20true=EB=A1=9C=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/writediary/screen/WriteDiaryScreen.kt | 5 ++++- .../ui/writediary/screen/WriteDiaryViewModel.kt | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index ba1ff28c..fd184a28 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -138,7 +138,10 @@ fun WriteDiaryRoute( onDismissLimitMessage = { viewModel.updateShowLimitMessage(false) }, onDismissEmptyFieldsMessage = { viewModel.updateShowEmptyFieldsMessage(false) }, onDismissFailureDialog = { viewModel.resetFailureDialog() }, - onDismissExitDialog = { viewModel.updateShowExitDialog(false) }, + onDismissExitDialog = { + viewModel.updateDraftUsage() + viewModel.updateShowExitDialog(false) + }, onConfirmExitDialog = { viewModel.updateShowExitDialog(false) navigateToPrevious() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index e8e5001e..289c9763 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE @@ -22,6 +23,7 @@ import javax.inject.Inject class WriteDiaryViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val networkUtil: NetworkUtil, + private val firstDraftLocalDataSource: FirstDraftLocalDataSource, ) : ViewModel() { private val _writeDiaryState = MutableStateFlow(WriteDiaryState.Idle) @@ -169,6 +171,13 @@ class WriteDiaryViewModel @Inject constructor( showExitDialog = show } + fun updateDraftUsage() { + if (!firstDraftLocalDataSource.isDraftUsed) { + firstDraftLocalDataSource.isDraftUsed = true + firstDraftLocalDataSource.isFirstUse = true + } + } + companion object { const val MAX_ENTRIES = 5 const val ENTRY_REGEX = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣\\W]{2,50}$" From 829c09107f2d235ec5c683dc26d0cde6682594d8 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 4 Jun 2025 16:11:55 +0900 Subject: [PATCH 088/299] =?UTF-8?q?[FEAT/#268]=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=EC=84=9C=20isFirstUse=EA=B0=80=20true?= =?UTF-8?q?=EC=9D=BC=EB=95=8C=20=EC=9D=B4=EC=96=B4=EC=93=B0=EA=B8=B0=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=84=A4=EC=A0=95=20=EC=9C=A0=EB=8F=84=20?= =?UTF-8?q?=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=EB=A5=BC=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/home/screen/HomeScreen.kt | 59 +++++++++++++++++++ .../ui/home/screen/HomeViewModel.kt | 10 ++++ 2 files changed, 69 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 20a77086..949823e9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -3,20 +3,25 @@ package com.sopt.clody.presentation.ui.home.screen import android.app.Activity import androidx.activity.compose.BackHandler import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -24,6 +29,7 @@ import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet +import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.dialog.ClodyDialog import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet import com.sopt.clody.presentation.ui.component.timepicker.YearMonthPicker @@ -56,6 +62,7 @@ fun HomeRoute( val calendarState by homeViewModel.calendarState.collectAsStateWithLifecycle() val dailyDiariesState by homeViewModel.dailyDiariesState.collectAsStateWithLifecycle() val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() + val showFirstDraftPopup by homeViewModel.showFirstDraftPopup.collectAsStateWithLifecycle() val isError = calendarState is CalendarState.Error || dailyDiariesState is DailyDiariesState.Error val errorMessage = when { @@ -112,6 +119,58 @@ fun HomeRoute( selectedYear = selectedYear, selectedMonth = selectedMonth, ) + + if (showFirstDraftPopup) { + ClodyPopupBottomSheet( + onDismissRequest = { homeViewModel.updateFirstDraftUse(false) }, + content = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp) + .padding(horizontal = 16.dp), + ) { + Text( + text = "기한이 지나면\n로디의 답장을 받을 수 없어요!", + color = ClodyTheme.colors.gray01, + textAlign = TextAlign.Center, + style = ClodyTheme.typography.head3, + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = "답장 마감 전에 일기를 이어쓸 수 있도록\n알려드리기 위해서는 알림 설정이 필요해요.", + color = ClodyTheme.colors.gray04, + textAlign = TextAlign.Center, + style = ClodyTheme.typography.body3Regular, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "[설정 > 애플리케이션 > 클로디 > 알림 > 알림표시]", + color = ClodyTheme.colors.gray04, + textAlign = TextAlign.Center, + style = ClodyTheme.typography.body3Regular, + ) + Spacer(modifier = Modifier.height(28.dp)) + ClodyButton( + text = "알림 받기", + onClick = {}, + enabled = true, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = "다음에 하기", + modifier = Modifier + .clickable(onClick = { homeViewModel.updateFirstDraftUse(false) }) + .padding(12.dp), + color = ClodyTheme.colors.gray05, + style = ClodyTheme.typography.body4Medium, + ) + Spacer(modifier = Modifier.height(4.dp)) + } + }, + ) + } } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 86c97b0f..0db24dc3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -2,6 +2,7 @@ package com.sopt.clody.presentation.ui.home.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.util.NetworkUtil @@ -19,6 +20,7 @@ import javax.inject.Inject class HomeViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val networkUtil: NetworkUtil, + private val firstDraftLocalDataSource: FirstDraftLocalDataSource, ) : ViewModel() { private val _calendarState = MutableStateFlow>(CalendarState.Idle) @@ -62,6 +64,9 @@ class HomeViewModel @Inject constructor( private val _showDiaryDeleteDialog = MutableStateFlow(false) val showDiaryDeleteDialog: StateFlow get() = _showDiaryDeleteDialog + private val _showFirstDraftPopup = MutableStateFlow(firstDraftLocalDataSource.isFirstUse) + val showFirstDraftPopup: StateFlow = _showFirstDraftPopup + private val _errorState = MutableStateFlow>(false to "") val errorState: StateFlow> = _errorState @@ -179,4 +184,9 @@ class HomeViewModel @Inject constructor( fun setShowDiaryDeleteDialog(state: Boolean) { _showDiaryDeleteDialog.value = state } + + fun updateFirstDraftUse(newState: Boolean) { + firstDraftLocalDataSource.isFirstUse = newState + _showFirstDraftPopup.value = newState + } } From 3b05efd87e2ef86e65fc324e269aefee46037866 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:36:29 +0900 Subject: [PATCH 089/299] =?UTF-8?q?[ADD/#265]=20Kotest=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20JUnit=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 10 ++++++++++ gradle/libs.versions.toml | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2eafa6da..d2b8ce36 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,6 +77,11 @@ android { buildConfig = true compose = true } + testOptions { + unitTests.all { + it.useJUnitPlatform() + } + } } dependencies { @@ -123,6 +128,11 @@ dependencies { // Mavericks implementation(libs.bundles.mavericks) + // Kotest + testImplementation(libs.bundles.kotest) + testImplementation(libs.mockk) + testImplementation(libs.coroutines.test) + // ETC implementation(libs.timber) implementation(libs.lottie.compose) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bb253a92..c50e6de2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -47,8 +47,9 @@ accompanist = "0.25.1" accompanist-insets = "0.28.0" firebase-config-ktx = "22.1.0" - mavericks = "3.0.9" +kotestVersion = "5.9.0" +mockk = "1.13.10" [libraries] # AndroidX Core @@ -122,6 +123,13 @@ mavericks = { module = "com.airbnb.android:mavericks", version.ref = "mavericks" mavericks-compose = { module = "com.airbnb.android:mavericks-compose", version.ref = "mavericks" } mavericks-hilt = { module = "com.airbnb.android:mavericks-hilt", version.ref = "mavericks" } +# Kotest +kotest-runner = { group = "io.kotest", name = "kotest-runner-junit5", version.ref = "kotestVersion" } +kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotestVersion" } +kotest-property = { group = "io.kotest", name = "kotest-property", version.ref = "kotestVersion" } + +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } @@ -159,7 +167,6 @@ test = [ "androidx-junit", "espresso-core", "ui-test-junit4", - "coroutines-test" ] debug = [ @@ -193,3 +200,9 @@ mavericks = [ "mavericks-compose", "mavericks-hilt" ] + +kotest = [ + "kotest-runner", + "kotest-assertions", + "kotest-property" +] From 8309c17783fe54f074ca228a9843999b125dceb8 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:37:58 +0900 Subject: [PATCH 090/299] =?UTF-8?q?[ADD/#265]=20DraftDiaryContents=20?= =?UTF-8?q?=EB=B0=8F=20CreatedDraftDiaryInfo=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt | 5 +++++ .../java/com/sopt/clody/domain/model/DraftDiaryContents.kt | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt create mode 100644 app/src/main/java/com/sopt/clody/domain/model/DraftDiaryContents.kt diff --git a/app/src/main/java/com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt b/app/src/main/java/com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt new file mode 100644 index 00000000..b8ea1325 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt @@ -0,0 +1,5 @@ +package com.sopt.clody.domain.model + +data class CreatedDraftDiaryInfo( + val createdAt: String, +) diff --git a/app/src/main/java/com/sopt/clody/domain/model/DraftDiaryContents.kt b/app/src/main/java/com/sopt/clody/domain/model/DraftDiaryContents.kt new file mode 100644 index 00000000..45ddcbd1 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/model/DraftDiaryContents.kt @@ -0,0 +1,5 @@ +package com.sopt.clody.domain.model + +data class DraftDiaryContents( + val draftDiaries: List, +) From ba09f76efd89dc193838b7e1b3ce777ac8e7ec15 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:38:10 +0900 Subject: [PATCH 091/299] =?UTF-8?q?[ADD/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20Response/Req?= =?UTF-8?q?uest=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/SaveDraftDiaryRequestDto.kt | 9 +++++++++ .../dto/response/DraftDiariesResponseDto.kt | 14 ++++++++++++++ .../dto/response/DraftDiaryCreatedResponseDto.kt | 16 ++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt create mode 100644 app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiariesResponseDto.kt create mode 100644 app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiaryCreatedResponseDto.kt diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt new file mode 100644 index 00000000..63b1bf84 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt @@ -0,0 +1,9 @@ +package com.sopt.clody.data.remote.dto.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class SaveDraftDiaryRequestDto( + @SerialName("draftDiaries") val draftDiaries: List, +) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiariesResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiariesResponseDto.kt new file mode 100644 index 00000000..6bb522b5 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiariesResponseDto.kt @@ -0,0 +1,14 @@ +package com.sopt.clody.data.remote.dto.response + +import com.sopt.clody.domain.model.DraftDiaryContents +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DraftDiariesResponseDto( + @SerialName("draftDiaries") val draftDiaries: List, +) { + fun toDomain() = DraftDiaryContents( + draftDiaries = draftDiaries ?: emptyList(), + ) +} diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiaryCreatedResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiaryCreatedResponseDto.kt new file mode 100644 index 00000000..1c6f3d8d --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiaryCreatedResponseDto.kt @@ -0,0 +1,16 @@ + +package com.sopt.clody.data.remote.dto.response + +import com.sopt.clody.domain.model.CreatedDraftDiaryInfo +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DraftDiaryCreatedResponseDto( + @SerialName("createdAt") + val createdAt: String, +) { + fun toDomain() = CreatedDraftDiaryInfo( + createdAt = createdAt, + ) +} From bf282eb4edb7b7fe297c39db78d2264a068ae241 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:38:24 +0900 Subject: [PATCH 092/299] =?UTF-8?q?[FEAT/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20Diary=20API=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/data/remote/api/DiaryService.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt b/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt index 8a702f80..20519223 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt @@ -1,9 +1,12 @@ package com.sopt.clody.data.remote.api import com.sopt.clody.data.remote.dto.base.ApiResponse +import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.request.WriteDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto +import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto +import com.sopt.clody.data.remote.dto.response.DraftDiaryCreatedResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto @@ -59,4 +62,16 @@ interface DiaryService { @Query("month") month: Int, @Query("date") date: Int, ): ApiResponse + + @GET("api/v1/draft") + suspend fun fetchDraftDiary( + @Query("year") year: Int, + @Query("month") month: Int, + @Query("date") date: Int, + ): ApiResponse + + @POST("api/v1/draft") + suspend fun saveDraftDiary( + @Body request: SaveDraftDiaryRequestDto, + ): ApiResponse } From ffdab1bcbaeea032c5ef01fe18e96cbcfc61f65f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:38:36 +0900 Subject: [PATCH 093/299] =?UTF-8?q?[FEAT/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20Diary=20=EA=B4=80=EB=A0=A8=20API=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80(dataSource)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/remote/datasource/DiaryRemoteDataSource.kt | 5 +++++ .../remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt index 19dae1a7..b6f740d3 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt @@ -1,8 +1,11 @@ package com.sopt.clody.data.remote.datasource import com.sopt.clody.data.remote.dto.base.ApiResponse +import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto +import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto +import com.sopt.clody.data.remote.dto.response.DraftDiaryCreatedResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto @@ -16,4 +19,6 @@ interface DiaryRemoteDataSource { suspend fun getMonthlyCalendarData(year: Int, month: Int): ApiResponse suspend fun getMonthlyDiary(year: Int, month: Int): ApiResponse suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse + suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): ApiResponse + suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse } diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt index cbbb7b53..6cebf748 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt @@ -3,9 +3,12 @@ package com.sopt.clody.data.remote.datasourceimpl import com.sopt.clody.data.remote.api.DiaryService import com.sopt.clody.data.remote.datasource.DiaryRemoteDataSource import com.sopt.clody.data.remote.dto.base.ApiResponse +import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.request.WriteDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto +import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto +import com.sopt.clody.data.remote.dto.response.DraftDiaryCreatedResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto @@ -35,4 +38,10 @@ class DiaryRemoteDataSourceImpl @Inject constructor( override suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse = diaryService.getReplyDiary(year = year, month = month, date = date) + + override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): ApiResponse = + diaryService.fetchDraftDiary(year = year, month = month, date = date) + + override suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse = + diaryService.saveDraftDiary(request) } From 54ab943168d680253b4c302ddeae7e56b0173aae Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:38:46 +0900 Subject: [PATCH 094/299] =?UTF-8?q?[FEAT/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EB=A6=AC=20API=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80(Repository)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositoryimpl/DiaryRepositoryImpl.kt | 21 +++++++++++++++++++ .../domain/repository/DiaryRepository.kt | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt index 20d14634..a32e04c9 100644 --- a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt @@ -1,6 +1,7 @@ package com.sopt.clody.data.repositoryimpl import com.sopt.clody.data.remote.datasource.DiaryRemoteDataSource +import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto @@ -8,6 +9,8 @@ import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto import com.sopt.clody.data.remote.util.handleApiResponse +import com.sopt.clody.domain.model.CreatedDraftDiaryInfo +import com.sopt.clody.domain.model.DraftDiaryContents import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.presentation.utils.network.ErrorMessages import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE @@ -80,4 +83,22 @@ class DiaryRepositoryImpl @Inject constructor( } response } + + override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result = + runCatching { + diaryRemoteDataSource + .fetchDraftDiary(year, month, date) + .handleApiResponse() + .getOrThrow() + .toDomain() + } + + override suspend fun saveDraftDiary(contents: List): Result = + runCatching { + diaryRemoteDataSource + .saveDraftDiary(SaveDraftDiaryRequestDto(draftDiaries = contents)) + .handleApiResponse() + .getOrThrow() + .toDomain() + } } diff --git a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt index 9c95565e..486f52ba 100644 --- a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt +++ b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt @@ -6,6 +6,8 @@ import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto +import com.sopt.clody.domain.model.CreatedDraftDiaryInfo +import com.sopt.clody.domain.model.DraftDiaryContents interface DiaryRepository { suspend fun writeDiary(date: String, content: List): Result @@ -15,4 +17,6 @@ interface DiaryRepository { suspend fun getMonthlyCalendarData(year: Int, month: Int): Result suspend fun getMonthlyDiary(year: Int, month: Int): Result suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result + suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result + suspend fun saveDraftDiary(contents: List): Result } From 761fa5526ccf173386d10205e45a1893ce99bd78 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:39:03 +0900 Subject: [PATCH 095/299] =?UTF-8?q?[FEAT/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20UseCase=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/domain/usecase/FetchDraftDiaryUseCase.kt | 13 +++++++++++++ .../clody/domain/usecase/SaveDraftDiaryUseCase.kt | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt create mode 100644 app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt new file mode 100644 index 00000000..929c2e1f --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt @@ -0,0 +1,13 @@ +package com.sopt.clody.domain.usecase + +import com.sopt.clody.domain.model.DraftDiaryContents +import com.sopt.clody.domain.repository.DiaryRepository +import javax.inject.Inject + +class FetchDraftDiaryUseCase @Inject constructor( + private val repository: DiaryRepository, +) { + suspend operator fun invoke(year: Int, month: Int, day: Int): Result { + return repository.fetchDraftDiary(year, month, day) + } +} diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt new file mode 100644 index 00000000..8a66fee0 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt @@ -0,0 +1,13 @@ +package com.sopt.clody.domain.usecase + +import com.sopt.clody.domain.model.CreatedDraftDiaryInfo +import com.sopt.clody.domain.repository.DiaryRepository +import javax.inject.Inject + +class SaveDraftDiaryUseCase @Inject constructor( + private val diaryRepository: DiaryRepository, +) { + suspend operator fun invoke(contents: List): Result { + return diaryRepository.saveDraftDiary(contents) + } +} From 9e9a87c3aee641d4d210c345ee82ff743d537edd Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:39:31 +0900 Subject: [PATCH 096/299] =?UTF-8?q?[FEAT/#265]=20ViewModel=EC=97=90=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=20=EC=9D=BC=EA=B8=B0=20fetch/save=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../writediary/screen/WriteDiaryViewModel.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index e8e5001e..a99df0f5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -9,6 +9,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository +import com.sopt.clody.domain.usecase.FetchDraftDiaryUseCase +import com.sopt.clody.domain.usecase.SaveDraftDiaryUseCase import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR @@ -21,6 +23,8 @@ import javax.inject.Inject @HiltViewModel class WriteDiaryViewModel @Inject constructor( private val diaryRepository: DiaryRepository, + private val fetchDraftDiaryUseCase: FetchDraftDiaryUseCase, + private val saveDraftDiaryUseCase: SaveDraftDiaryUseCase, private val networkUtil: NetworkUtil, ) : ViewModel() { @@ -57,6 +61,8 @@ class WriteDiaryViewModel @Inject constructor( var showExitDialog by mutableStateOf(false) private set + private var initialEntries: List = emptyList() + fun writeDiary(year: Int, month: Int, day: Int, contents: List) { viewModelScope.launch { if (!networkUtil.isNetworkAvailable()) { @@ -169,6 +175,44 @@ class WriteDiaryViewModel @Inject constructor( showExitDialog = show } + fun hasChangedFromInitial(): Boolean { + return entries != initialEntries + } + + fun fetchDraftDiary(year: Int, month: Int, day: Int) { + viewModelScope.launch { + val result = fetchDraftDiaryUseCase(year, month, day) + result.onSuccess { response -> + _entries.clear() + _entries.addAll(response.draftDiaries) + + initialEntries = response.draftDiaries.toList() + + _showWarnings.clear() + _showWarnings.addAll(List(_entries.size) { false }) + checkLimitMessage() + checkEmptyFieldsMessage() + }.onFailure { + _failureMessage.value = it.localizedMessage ?: UNKNOWN_ERROR + _showFailureDialog.value = true + } + } + } + + fun saveDraftDiary() { + viewModelScope.launch { + val result = saveDraftDiaryUseCase(_entries.toList()) + result.onSuccess { + _failureMessage.value = "" + _showFailureDialog.value = false + println("성공함") + }.onFailure { e -> + _failureMessage.value = e.localizedMessage ?: UNKNOWN_ERROR + _showFailureDialog.value = true + } + } + } + companion object { const val MAX_ENTRIES = 5 const val ENTRY_REGEX = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣\\W]{2,50}$" From c80e41822ec69e643e0473bb3a7c539f5c4c516d Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:39:41 +0900 Subject: [PATCH 097/299] =?UTF-8?q?[ADD/#265]=20Lifecycle=20STARTED=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20LaunchedEffect=20=EC=9C=A0=ED=8B=B8=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/utils/extension/LifeCycle.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt index 61458078..8e0208ab 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/LifeCycle.kt @@ -1,7 +1,10 @@ package com.sopt.clody.presentation.utils.extension +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.CoroutineScope @@ -25,3 +28,28 @@ fun LifecycleOwner.repeatOnStarted(block: suspend CoroutineScope.() -> Unit) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block) } } + +/** + * [LifecycleOwner]가 [lifecycleState] 이상일 때만 [block]을 실행하는 [LaunchedEffect] 기반의 Composable 유틸함수. + * 내부적으로 [Lifecycle.repeatOnLifecycle]을 사용하여 생명주기 안전성을 보장. + * SideEffect 처리를 lifecycle-aware하게 실행하고 싶을 때 사용하면 됨. + * + * @param key [LaunchedEffect]를 트리거할 key. 일반적으로 의존성이 되는 상태나 객체. + * @param lifecycleState 반복 실행을 시작할 최소 생명주기 상태 (기본값: STARTED) + * @param block 지정한 생명주기 상태 이상일 때만 실행할 suspend 블록 + */ + +@Composable +fun LaunchedEffectWhenStarted( + key: Any? = Unit, + lifecycleState: Lifecycle.State = Lifecycle.State.STARTED, + block: suspend CoroutineScope.() -> Unit, +) { + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(key, lifecycleOwner) { + lifecycleOwner.lifecycle.repeatOnLifecycle(lifecycleState) { + block() + } + } +} From d822f935812bb3f9c36a6e1c685bcdb077cd1993 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:40:09 +0900 Subject: [PATCH 098/299] =?UTF-8?q?[ADD/#265]=20route=EB=8B=A8=EC=97=90=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20fetch=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=20?= =?UTF-8?q?=EB=B6=84=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 --- .../ui/writediary/screen/WriteDiaryScreen.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index ba1ff28c..073fb910 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -48,6 +48,7 @@ import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.base.BasePreview import com.sopt.clody.presentation.utils.base.ClodyPreview +import com.sopt.clody.presentation.utils.extension.LaunchedEffectWhenStarted import com.sopt.clody.presentation.utils.extension.getDayOfWeek import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage import com.sopt.clody.ui.theme.ClodyTheme @@ -74,6 +75,10 @@ fun WriteDiaryRoute( val showDialog by viewModel::showDialog val showExitDialog by viewModel::showExitDialog + LaunchedEffectWhenStarted { + viewModel.fetchDraftDiary(year, month, date) + } + LaunchedEffect(writeDiaryState) { when (writeDiaryState) { is WriteDiaryState.Success -> navigateToReplyLoading(year, month, date) @@ -95,7 +100,7 @@ fun WriteDiaryRoute( failureMessage = failureMessage, showExitDialog = showExitDialog, onClickBack = { - if (entries.any { it.isNotBlank() }) { + if (viewModel.hasChangedFromInitial()) { AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_BACK) viewModel.updateShowExitDialog(true) } else { @@ -138,7 +143,11 @@ fun WriteDiaryRoute( onDismissLimitMessage = { viewModel.updateShowLimitMessage(false) }, onDismissEmptyFieldsMessage = { viewModel.updateShowEmptyFieldsMessage(false) }, onDismissFailureDialog = { viewModel.resetFailureDialog() }, - onDismissExitDialog = { viewModel.updateShowExitDialog(false) }, + onDismissExitDialog = { + viewModel.updateShowExitDialog(false) + viewModel.saveDraftDiary() + navigateToPrevious() + }, onConfirmExitDialog = { viewModel.updateShowExitDialog(false) navigateToPrevious() From 6a1ebec37a824916ecbe4e0f3595b7e5ca6d5cec Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:40:46 +0900 Subject: [PATCH 099/299] =?UTF-8?q?[TEST/#265]=20FakeDiaryRepository=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 --- .../com/sopt/clody/FakeDiaryRepository.kt | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt diff --git a/app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt b/app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt new file mode 100644 index 00000000..9355bd74 --- /dev/null +++ b/app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt @@ -0,0 +1,55 @@ +package com.sopt.clody + +import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto +import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto +import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto +import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto +import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto +import com.sopt.clody.domain.model.CreatedDraftDiaryInfo +import com.sopt.clody.domain.model.DraftDiaryContents +import com.sopt.clody.domain.repository.DiaryRepository + +class FakeDiaryRepository : DiaryRepository { + var draftDiaryResult: Result? = null + var saveDraftResult: Result? = null + private var draftDiaryContents: DraftDiaryContents? = null + + override suspend fun writeDiary(date: String, content: List): Result { + throw NotImplementedError("This method is not implemented in FakeDiaryRepository") + } + + override suspend fun deleteDailyDiary(year: Int, month: Int, day: Int): Result { + throw NotImplementedError("This method is not implemented in FakeDiaryRepository") + } + + override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): Result { + throw NotImplementedError("This method is not implemented in FakeDiaryRepository") + } + + override suspend fun getDiaryTime(year: Int, month: Int, date: Int): Result { + throw NotImplementedError("This method is not implemented in FakeDiaryRepository") + } + + override suspend fun getMonthlyCalendarData(year: Int, month: Int): Result { + throw NotImplementedError("This method is not implemented in FakeDiaryRepository") + } + + override suspend fun getMonthlyDiary(year: Int, month: Int): Result { + throw NotImplementedError("This method is not implemented in FakeDiaryRepository") + } + + override suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result { + throw NotImplementedError("This method is not implemented in FakeDiaryRepository") + } + + override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result { + return draftDiaryContents?.let { Result.success(it) } + ?: Result.failure(IllegalStateException("No draft found")) + } + + override suspend fun saveDraftDiary(contents: List): Result { + draftDiaryContents = DraftDiaryContents(contents) + return Result.success(CreatedDraftDiaryInfo(createdAt = "2024-06-01T00:00:00")) + } +} From 64ff04420ce60d4451976509c3e3e9e123c0c198 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:41:18 +0900 Subject: [PATCH 100/299] =?UTF-8?q?[TEST/#265]=20FakeDiaryRemoteDataSource?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datasource/FakeDiaryRemoteDataSource.kt | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt diff --git a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt new file mode 100644 index 00000000..a7a6c2eb --- /dev/null +++ b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt @@ -0,0 +1,79 @@ +package com.sopt.clody.datasource + +import com.sopt.clody.data.remote.datasource.DiaryRemoteDataSource +import com.sopt.clody.data.remote.dto.base.ApiResponse +import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto +import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto +import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto +import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto +import com.sopt.clody.data.remote.dto.response.DraftDiaryCreatedResponseDto +import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto +import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto +import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto + +class FakeDiaryRemoteDataSource : DiaryRemoteDataSource { + + var draftDiariesResponse: ApiResponse? = null + var saveDraftResponse: ApiResponse? = null + + override suspend fun writeDiary(date: String, content: List): ApiResponse { + throw NotImplementedError() + } + + override suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): ApiResponse { + throw NotImplementedError() + } + + override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): ApiResponse { + throw NotImplementedError() + } + + override suspend fun getDiaryTime(year: Int, month: Int, date: Int): ApiResponse { + throw NotImplementedError() + } + + override suspend fun getMonthlyCalendarData(year: Int, month: Int): ApiResponse { + throw NotImplementedError() + } + + override suspend fun getMonthlyDiary(year: Int, month: Int): ApiResponse { + throw NotImplementedError() + } + + override suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse { + throw NotImplementedError() + } + + override suspend fun fetchDraftDiary( + year: Int, + month: Int, + date: Int, + ): ApiResponse { + return draftDiariesResponse + ?: throw IllegalStateException("draftDiariesResponse not set") + } + + override suspend fun saveDraftDiary( + request: SaveDraftDiaryRequestDto, + ): ApiResponse { + return saveDraftResponse + ?: throw IllegalStateException("saveDraftResponse not set") + } + + fun setDraftDiariesResponse(list: List) { + draftDiariesResponse = ApiResponse( + status = 200, + message = "성공", + data = DraftDiariesResponseDto(draftDiaries = list), + ) + } + + fun setSaveDraftDiaryResponse(createdAt: String) { + saveDraftResponse = ApiResponse( + status = 201, + message = "성공", + data = DraftDiaryCreatedResponseDto(createdAt), + ) + } +} From 62c351cc60f181aafb6b59e377428dd831fcd15c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:41:31 +0900 Subject: [PATCH 101/299] =?UTF-8?q?[TEST/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=8B=A4=EC=9D=B4=EC=96=B4=EB=A6=AC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EC=A0=80=EC=9E=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=85=8C=EC=8A=A4?= =?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 --- .../repositoryimpl/DiaryRepositoryImplTest.kt | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt diff --git a/app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt b/app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt new file mode 100644 index 00000000..6acd516d --- /dev/null +++ b/app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt @@ -0,0 +1,59 @@ +package com.sopt.clody.repositoryimpl + +import com.sopt.clody.data.repositoryimpl.DiaryRepositoryImpl +import com.sopt.clody.datasource.FakeDiaryRemoteDataSource +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe + +class DiaryRepositoryImplTest : BehaviorSpec({ + + Given("임시저장 다이어리 조회 기능") { + + val fakeDiaryRemoteDataSource = FakeDiaryRemoteDataSource() + val diaryRepository = DiaryRepositoryImpl(fakeDiaryRemoteDataSource) + + When("유효한 날짜를 전달받으면") { + + Then("해당 날짜의 임시저장 데이터를 정상적으로 반환한다") { + // arrange + val mockData = listOf("오늘도 고생했어", "하루 정리 완료") + fakeDiaryRemoteDataSource.setDraftDiariesResponse(mockData) + + // act + val result = diaryRepository.fetchDraftDiary(2025, 5, 31) + + // assert + println("fetchDraftDiary result: $result") + println("fetchDraftDiary error: ${result.exceptionOrNull()?.message}") + + result.isSuccess shouldBe true + result.getOrNull()?.draftDiaries shouldBe mockData + } + } + } + + Given("임시저장 다이어리 저장 기능") { + + val fakeDiaryRemoteDataSource = FakeDiaryRemoteDataSource() + val diaryRepository = DiaryRepositoryImpl(fakeDiaryRemoteDataSource) + + When("유효한 데이터로 저장 요청을 하면") { + + Then("정상적으로 저장된 시간 정보를 반환한다") { + // arrange + val createdAt = "2025-05-31T20:43:20.696606" + fakeDiaryRemoteDataSource.setSaveDraftDiaryResponse(createdAt) + + // act + val result = diaryRepository.saveDraftDiary(listOf("Test")) + + // assert + println("saveDraftDiary result: $result") + println("saveDraftDiary error: ${result.exceptionOrNull()?.message}") + + result.isSuccess shouldBe true + result.getOrNull()?.createdAt shouldBe createdAt + } + } + } +},) From d674b069c918dbeca4fd2f0ae0316957bd74adef Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 4 Jun 2025 16:41:39 +0900 Subject: [PATCH 102/299] =?UTF-8?q?[TEST/#265]=20WriteDiaryViewModel=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=ED=95=9C?= =?UTF-8?q?=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/clody/WriteDiaryViewModelTest.kt | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt diff --git a/app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt b/app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt new file mode 100644 index 00000000..31c1a2bc --- /dev/null +++ b/app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt @@ -0,0 +1,86 @@ +package com.sopt.clody + +import com.sopt.clody.domain.model.CreatedDraftDiaryInfo +import com.sopt.clody.domain.model.DraftDiaryContents +import com.sopt.clody.domain.usecase.FetchDraftDiaryUseCase +import com.sopt.clody.domain.usecase.SaveDraftDiaryUseCase +import com.sopt.clody.presentation.ui.writediary.screen.WriteDiaryViewModel +import io.kotest.assertions.nondeterministic.eventually +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +class WriteDiaryViewModelTest : BehaviorSpec( + { + + val testDispatcher = UnconfinedTestDispatcher() + + beforeTest { + Dispatchers.setMain(testDispatcher) + } + + afterTest { + Dispatchers.resetMain() + } + + Given("fetchDraftDiary 호출 시") { + val mockContents = listOf("entry1", "entry2", "entry3") + val fakeRepo = FakeDiaryRepository().apply { + draftDiaryResult = Result.success(DraftDiaryContents(mockContents)) + } + + val viewModel = WriteDiaryViewModel( + diaryRepository = fakeRepo, + fetchDraftDiaryUseCase = FetchDraftDiaryUseCase(fakeRepo), + saveDraftDiaryUseCase = SaveDraftDiaryUseCase(fakeRepo), + networkUtil = mockk(relaxed = true), + ) + + When("fetchDraftDiaryUseCase가 성공하면") { + viewModel.fetchDraftDiary(2025, 6, 1) + + Then("entries와 showWarnings가 초기화된다") { + eventually(duration = 2.seconds) { + viewModel.entries shouldContainExactly mockContents + viewModel.showWarnings shouldContainExactly List(mockContents.size) { false } + } + } + } + } + + Given("saveDraftDiary 호출 시") { + val fakeRepo = FakeDiaryRepository().apply { + saveDraftResult = Result.success(CreatedDraftDiaryInfo("2025-06-01T00:00:00.000Z")) + } + + val viewModel = WriteDiaryViewModel( + diaryRepository = fakeRepo, + fetchDraftDiaryUseCase = FetchDraftDiaryUseCase(fakeRepo), + saveDraftDiaryUseCase = SaveDraftDiaryUseCase(fakeRepo), + networkUtil = mockk(relaxed = true), + ) + + viewModel.updateEntry(0, "entry1") + viewModel.addEntry() + viewModel.updateEntry(1, "entry2") + + viewModel.saveDraftDiary() + + Then("에러 메시지가 설정되지 않는다") { + eventually(duration = 3.seconds) { + viewModel.showFailureDialog.value shouldBe false + viewModel.failureMessage.value shouldBe "" + println("dialog = ${viewModel.showFailureDialog.value}, message = ${viewModel.failureMessage.value}") + } + } + } + }, +) From 5dc4ab1ce152d2bfef51d24639c1e3e57583f08f Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 4 Jun 2025 17:30:01 +0900 Subject: [PATCH 103/299] =?UTF-8?q?[FEAT/#268]=20=EB=B0=94=ED=85=80?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EB=B0=9B=EA=B8=B0=EB=A5=BC=20=EC=84=A0=ED=83=9D=ED=95=A0=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0,=20=EC=9D=B4=EC=96=B4=EC=93=B0=EA=B8=B0=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=EC=9D=84=20=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20=EA=B5=AC=ED=98=84=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/home/screen/HomeScreen.kt | 24 ++++++- .../ui/home/screen/HomeViewModel.kt | 65 +++++++++++++++++++ .../ui/writediary/screen/WriteDiaryScreen.kt | 1 + 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 949823e9..97d7615c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sopt.clody.R import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen @@ -33,6 +34,7 @@ import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.dialog.ClodyDialog import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet import com.sopt.clody.presentation.ui.component.timepicker.YearMonthPicker +import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData import com.sopt.clody.presentation.ui.home.component.DiaryStateButton import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar @@ -63,6 +65,8 @@ fun HomeRoute( val dailyDiariesState by homeViewModel.dailyDiariesState.collectAsStateWithLifecycle() val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() val showFirstDraftPopup by homeViewModel.showFirstDraftPopup.collectAsStateWithLifecycle() + val draftAlarmEnableToast by homeViewModel.draftAlarmEnableToast.collectAsStateWithLifecycle() + val context = LocalContext.current val isError = calendarState is CalendarState.Error || dailyDiariesState is DailyDiariesState.Error val errorMessage = when { @@ -154,7 +158,10 @@ fun HomeRoute( Spacer(modifier = Modifier.height(28.dp)) ClodyButton( text = "알림 받기", - onClick = {}, + onClick = { + homeViewModel.enableDraftAlarm(context) + homeViewModel.updateFirstDraftUse(false) + }, enabled = true, modifier = Modifier.fillMaxWidth(), ) @@ -171,6 +178,17 @@ fun HomeRoute( }, ) } + + if (draftAlarmEnableToast) { + ClodyToastMessage( + message = "이어쓰기 알림 설정을 완료했어요.", + iconResId = R.drawable.ic_toast_check_on_18, + backgroundColor = ClodyTheme.colors.gray04, + contentColor = ClodyTheme.colors.white, + durationMillis = 3000, + onDismiss = { homeViewModel.resetDraftAlarmEnableToast() }, + ) + } } } @@ -242,7 +260,7 @@ fun HomeScreen( containerColor = ClodyTheme.colors.white, content = { innerPadding -> when (val state = calendarState) { - is CalendarState.Idle -> { } + is CalendarState.Idle -> {} is CalendarState.Loading -> { LoadingScreen() @@ -270,7 +288,7 @@ fun HomeScreen( } when (deleteDiaryState) { - is DeleteDiaryState.Idle -> { } + is DeleteDiaryState.Idle -> {} is DeleteDiaryState.Loading -> { LoadingScreen() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 0db24dc3..ef9d4f4a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -1,13 +1,19 @@ package com.sopt.clody.presentation.ui.home.screen +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.ClodyFirebaseMessagingService.Companion.getTokenFromPreferences import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource +import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository +import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData +import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState import com.sopt.clody.presentation.utils.network.ErrorMessages import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -19,6 +25,7 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val diaryRepository: DiaryRepository, + private val notificationRepository: NotificationRepository, private val networkUtil: NetworkUtil, private val firstDraftLocalDataSource: FirstDraftLocalDataSource, ) : ViewModel() { @@ -67,6 +74,12 @@ class HomeViewModel @Inject constructor( private val _showFirstDraftPopup = MutableStateFlow(firstDraftLocalDataSource.isFirstUse) val showFirstDraftPopup: StateFlow = _showFirstDraftPopup + private val _draftAlarmChangeState = MutableStateFlow(NotificationChangeState.Idle) + val draftAlarmChangeState: StateFlow = _draftAlarmChangeState + + private val _draftAlarmEnableToast = MutableStateFlow(false) + val draftAlarmEnableToast: StateFlow = _draftAlarmEnableToast + private val _errorState = MutableStateFlow>(false to "") val errorState: StateFlow> = _errorState @@ -189,4 +202,56 @@ class HomeViewModel @Inject constructor( firstDraftLocalDataSource.isFirstUse = newState _showFirstDraftPopup.value = newState } + + fun enableDraftAlarm(context: Context) { + viewModelScope.launch { + val fcmToken = getFcmToken(context) ?: return@launch + val notificationInfo = getNotificationInfo(fcmToken) ?: return@launch + val request = buildDraftAlarmRequest(notificationInfo, fcmToken) + sendDraftAlarmRequest(request) + } + } + + private suspend fun getFcmToken(context: Context): String? { + val token = getTokenFromPreferences(context) + if (token.isNullOrBlank()) { + _draftAlarmChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") + return null + } + return token + } + + private suspend fun getNotificationInfo(fcmToken: String): NotificationInfoResponseDto? { + return notificationRepository.getNotificationInfo().getOrElse { + _draftAlarmChangeState.value = NotificationChangeState.Failure("알림 정보를 가져오는데 실패했습니다.") + null + } + } + + private fun buildDraftAlarmRequest( + info: NotificationInfoResponseDto, + fcmToken: String, + ): SendNotificationRequestDto = SendNotificationRequestDto( + isDiaryAlarm = info.isDiaryAlarm, + isDraftAlarm = true, + isReplyAlarm = info.isReplyAlarm, + time = if (info.time != "21:30") info.time else "21:30", + fcmToken = fcmToken, + ) + + private suspend fun sendDraftAlarmRequest(request: SendNotificationRequestDto) { + notificationRepository.sendNotification(request).fold( + onSuccess = { + _draftAlarmEnableToast.value = true + _draftAlarmChangeState.value = NotificationChangeState.Success(it) + }, + onFailure = { + _draftAlarmChangeState.value = NotificationChangeState.Failure("이어쓰기 알림 설정에 실패했습니다.") + }, + ) + } + + fun resetDraftAlarmEnableToast() { + _draftAlarmEnableToast.value = false + } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index fd184a28..1c13789a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -141,6 +141,7 @@ fun WriteDiaryRoute( onDismissExitDialog = { viewModel.updateDraftUsage() viewModel.updateShowExitDialog(false) + navigateToPrevious() }, onConfirmExitDialog = { viewModel.updateShowExitDialog(false) From f2ca24a0d22aeedec85faf7d5b626ed101e01123 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 4 Jun 2025 17:31:43 +0900 Subject: [PATCH 104/299] =?UTF-8?q?[CHORE/#268]=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=9C=EA=B1=B0=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/presentation/ui/home/screen/HomeViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index ef9d4f4a..93692ebc 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -206,7 +206,7 @@ class HomeViewModel @Inject constructor( fun enableDraftAlarm(context: Context) { viewModelScope.launch { val fcmToken = getFcmToken(context) ?: return@launch - val notificationInfo = getNotificationInfo(fcmToken) ?: return@launch + val notificationInfo = getNotificationInfo() ?: return@launch val request = buildDraftAlarmRequest(notificationInfo, fcmToken) sendDraftAlarmRequest(request) } @@ -221,7 +221,7 @@ class HomeViewModel @Inject constructor( return token } - private suspend fun getNotificationInfo(fcmToken: String): NotificationInfoResponseDto? { + private suspend fun getNotificationInfo(): NotificationInfoResponseDto? { return notificationRepository.getNotificationInfo().getOrElse { _draftAlarmChangeState.value = NotificationChangeState.Failure("알림 정보를 가져오는데 실패했습니다.") null From ab6eba09d54a7c2eda0a8b8b398e8ae42ef9f647 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 4 Jun 2025 18:23:16 +0900 Subject: [PATCH 105/299] =?UTF-8?q?[REFACTOR/#268]=20HomeScreen=EC=9D=84?= =?UTF-8?q?=20Recomposition=ED=95=B4=EC=84=9C=20=EB=B0=94=ED=85=80?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=20=EB=85=B8=EC=B6=9C=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=EA=B0=80=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A3=A8=EC=96=B4=EC=A7=80=EB=8F=84=EB=A1=9D=20naviga?= =?UTF-8?q?teToHome=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index 1c13789a..da7ec6bd 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -141,7 +141,7 @@ fun WriteDiaryRoute( onDismissExitDialog = { viewModel.updateDraftUsage() viewModel.updateShowExitDialog(false) - navigateToPrevious() + navigateToHome(year, month) }, onConfirmExitDialog = { viewModel.updateShowExitDialog(false) From d3ecd2ebc75cb13d47884903f630c6a797ef33fb Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 4 Jun 2025 18:25:55 +0900 Subject: [PATCH 106/299] =?UTF-8?q?[CHORE/#268]=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=EC=97=B0=EA=B2=B0=20=EA=B2=80=EC=82=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=98=EA=B3=A0,=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=8B=9C=EA=B0=84=20=EA=B2=80=EC=A6=9D=EC=9D=84=20?= =?UTF-8?q?=ED=9A=A8=EA=B3=BC=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/ui/home/screen/HomeViewModel.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 93692ebc..91c05c3c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -205,6 +205,11 @@ class HomeViewModel @Inject constructor( fun enableDraftAlarm(context: Context) { viewModelScope.launch { + if (!networkUtil.isNetworkAvailable()) { + setErrorState(true, ErrorMessages.FAILURE_NETWORK_MESSAGE) + return@launch + } + val fcmToken = getFcmToken(context) ?: return@launch val notificationInfo = getNotificationInfo() ?: return@launch val request = buildDraftAlarmRequest(notificationInfo, fcmToken) @@ -235,7 +240,7 @@ class HomeViewModel @Inject constructor( isDiaryAlarm = info.isDiaryAlarm, isDraftAlarm = true, isReplyAlarm = info.isReplyAlarm, - time = if (info.time != "21:30") info.time else "21:30", + time = info.time.ifEmpty { "21:30" }, fcmToken = fcmToken, ) From d31441b7e5d77817796c9f8d60949b58304ace66 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 5 Jun 2025 14:52:25 +0900 Subject: [PATCH 107/299] =?UTF-8?q?[CHORE/#268]=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=EB=8A=94=20=EC=9D=B4=EC=96=B4=EC=93=B0?= =?UTF-8?q?=EA=B8=B0=20=EC=95=8C=EB=A6=BC=EC=9D=84=20=EB=B9=84=ED=99=9C?= =?UTF-8?q?=EC=84=B1=ED=99=94=20=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/auth/timereminder/TimeReminderViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt index ee63c6de..c8e45835 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt @@ -45,7 +45,7 @@ class TimeReminderViewModel @Inject constructor( val requestDto = SendNotificationRequestDto( isDiaryAlarm = isPermissionGranted, - isDraftAlarm = isPermissionGranted, + isDraftAlarm = false, isReplyAlarm = isPermissionGranted, time = selectedTime, fcmToken = fcmToken, From 236f58c2852faeb7660adbc0bc7e433ae03b3a22 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 5 Jun 2025 15:21:59 +0900 Subject: [PATCH 108/299] =?UTF-8?q?[ADD/#273]=20=ED=94=8C=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=ED=86=A0=EC=96=B4=20=EC=9D=B8=EC=95=B1=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 3 +++ gradle/libs.versions.toml | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2eafa6da..e62a75f9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -123,6 +123,9 @@ dependencies { // Mavericks implementation(libs.bundles.mavericks) + // Play Store + implementation(libs.bundles.plays) + // ETC implementation(libs.timber) implementation(libs.lottie.compose) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bb253a92..fb65e83e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,8 @@ firebase-config-ktx = "22.1.0" mavericks = "3.0.9" +play-review = "2.0.2" + [libraries] # AndroidX Core core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } @@ -122,6 +124,10 @@ mavericks = { module = "com.airbnb.android:mavericks", version.ref = "mavericks" mavericks-compose = { module = "com.airbnb.android:mavericks-compose", version.ref = "mavericks" } mavericks-hilt = { module = "com.airbnb.android:mavericks-hilt", version.ref = "mavericks" } +# PlayStore In App Review +play-review = { group = "com.google.android.play", name = "review", version.ref = "play-review"} +play-review-ktx = { group = "com.google.android.play", name = "review-ktx", version.ref = "play-review"} + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } @@ -193,3 +199,8 @@ mavericks = [ "mavericks-compose", "mavericks-hilt" ] + +plays = [ + "play-review", + "play-review-ktx" +] From 402cde0aa4558672247c94fe72d5d12d056bfe0c Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 5 Jun 2025 15:22:44 +0900 Subject: [PATCH 109/299] =?UTF-8?q?[FEAT/#273]=20=EC=9D=B8=EC=95=B1=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C=20InAppReviewM?= =?UTF-8?q?anager=EB=A5=BC=20=EC=A0=95=EC=9D=98=ED=95=A9=EB=8B=88=EB=8B=A4?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/core/review/InAppReviewManager.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt diff --git a/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt b/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt new file mode 100644 index 00000000..960d611c --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt @@ -0,0 +1,25 @@ +package com.sopt.clody.core.review + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import com.google.android.play.core.review.ReviewManagerFactory + +object InAppReviewManager { + fun showPopup(activity: Activity) { + val reviewManager = ReviewManagerFactory.create(activity) + val request = reviewManager.requestReviewFlow() + + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + val reviewInfo = task.result + reviewManager.launchReviewFlow(activity, reviewInfo) + } else { + // fallback logic: 예를 들어 마켓 링크 열기 + val uri = Uri.parse("market://details?id=${activity.packageName}") + val intent = Intent(Intent.ACTION_VIEW, uri) + activity.startActivity(intent) + } + } + } +} From d0e33d4db7dbb7135424e76ed609736850739226 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 5 Jun 2025 15:23:42 +0900 Subject: [PATCH 110/299] =?UTF-8?q?[FEAT/#273]=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=ED=8C=9D=EC=97=85=EC=9D=84=20=EB=85=B8=EC=B6=9C=ED=95=A0=20?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EB=8C=80=ED=95=9C=20flag=EB=A5=BC=20?= =?UTF-8?q?=EB=8B=B4=EC=9D=80=20SharedPreferences=EB=A5=BC=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/datastore/TokenDataStoreImpl.kt | 3 ++- .../datasource/AppReviewLocalDataSource.kt | 5 +++++ .../AppReviewLocalDataSourceImpl.kt | 19 +++++++++++++++++++ .../sopt/clody/di/SharedPreferencesModule.kt | 17 +++++++++++++++++ .../com/sopt/clody/di/TokenDataStoreModule.kt | 3 ++- .../com/sopt/clody/di/qualifier/Qualifier.kt | 11 +++++++++++ 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/data/local/datasource/AppReviewLocalDataSource.kt create mode 100644 app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt create mode 100644 app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt diff --git a/app/src/main/java/com/sopt/clody/data/datastore/TokenDataStoreImpl.kt b/app/src/main/java/com/sopt/clody/data/datastore/TokenDataStoreImpl.kt index c71dda3f..04f62ef6 100644 --- a/app/src/main/java/com/sopt/clody/data/datastore/TokenDataStoreImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/datastore/TokenDataStoreImpl.kt @@ -1,10 +1,11 @@ package com.sopt.clody.data.datastore import android.content.SharedPreferences +import com.sopt.clody.di.qualifier.TokenPrefs import javax.inject.Inject class TokenDataStoreImpl @Inject constructor( - private val sharedPreferences: SharedPreferences, + @TokenPrefs private val sharedPreferences: SharedPreferences, ) : TokenDataStore { override var accessToken: String get() = sharedPreferences.getString(ACCESS_TOKEN, "") ?: "" diff --git a/app/src/main/java/com/sopt/clody/data/local/datasource/AppReviewLocalDataSource.kt b/app/src/main/java/com/sopt/clody/data/local/datasource/AppReviewLocalDataSource.kt new file mode 100644 index 00000000..4c2c5992 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/local/datasource/AppReviewLocalDataSource.kt @@ -0,0 +1,5 @@ +package com.sopt.clody.data.local.datasource + +interface AppReviewLocalDataSource { + var shouldShowPopup: Boolean +} diff --git a/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt new file mode 100644 index 00000000..3d20c1c3 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt @@ -0,0 +1,19 @@ +package com.sopt.clody.data.local.datasourceimpl + +import android.content.SharedPreferences +import com.sopt.clody.data.local.datasource.AppReviewLocalDataSource +import com.sopt.clody.di.qualifier.ReviewPrefs +import javax.inject.Inject + +class AppReviewLocalDataSourceImpl @Inject constructor( + @ReviewPrefs private val sharedPreferences: SharedPreferences, +) : AppReviewLocalDataSource { + + override var shouldShowPopup: Boolean + get() = sharedPreferences.getBoolean(SHOULD_SHOW_POPUP, true) + set(value) = sharedPreferences.edit().putBoolean(SHOULD_SHOW_POPUP, value).apply() + + companion object { + private const val SHOULD_SHOW_POPUP = "shouldShowPopup" + } +} diff --git a/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt b/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt index 4ea8c970..c27f9b34 100644 --- a/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt +++ b/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt @@ -2,6 +2,10 @@ package com.sopt.clody.di import android.content.Context import android.content.SharedPreferences +import com.sopt.clody.data.local.datasource.AppReviewLocalDataSource +import com.sopt.clody.data.local.datasourceimpl.AppReviewLocalDataSourceImpl +import com.sopt.clody.di.qualifier.ReviewPrefs +import com.sopt.clody.di.qualifier.TokenPrefs import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,7 +18,20 @@ import javax.inject.Singleton object SharedPreferencesModule { @Provides @Singleton + @TokenPrefs fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences { return context.getSharedPreferences("token_prefs", Context.MODE_PRIVATE) } + + @Provides + @Singleton + @ReviewPrefs + fun provideReviewSharedPreferences(@ApplicationContext context: Context): SharedPreferences { + return context.getSharedPreferences("review_prefs", Context.MODE_PRIVATE) + } + + @Provides + @Singleton + fun provideAppReviewLocalDataSource(@ReviewPrefs sharedPreferences: SharedPreferences): AppReviewLocalDataSource = + AppReviewLocalDataSourceImpl(sharedPreferences) } diff --git a/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt b/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt index 3f1462ad..9f33f96d 100644 --- a/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt +++ b/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt @@ -3,6 +3,7 @@ package com.sopt.clody.di import android.content.SharedPreferences import com.sopt.clody.data.datastore.TokenDataStore import com.sopt.clody.data.datastore.TokenDataStoreImpl +import com.sopt.clody.di.qualifier.TokenPrefs import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,7 +15,7 @@ import javax.inject.Singleton object TokenDataStoreModule { @Provides @Singleton - fun provideTokenDataStore(sharedPreferences: SharedPreferences): TokenDataStore { + fun provideTokenDataStore(@TokenPrefs sharedPreferences: SharedPreferences): TokenDataStore { return TokenDataStoreImpl(sharedPreferences) } } diff --git a/app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt b/app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt new file mode 100644 index 00000000..a61d98ef --- /dev/null +++ b/app/src/main/java/com/sopt/clody/di/qualifier/Qualifier.kt @@ -0,0 +1,11 @@ +package com.sopt.clody.di.qualifier + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class TokenPrefs + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ReviewPrefs From 0cc06d05e7e67a7ee6d4b6d977ca02dfc7668c67 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 5 Jun 2025 15:24:00 +0900 Subject: [PATCH 111/299] =?UTF-8?q?[FEAT/#273]=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=90=EC=84=9C=20=EB=A6=AC=EB=B7=B0=20=ED=8C=9D?= =?UTF-8?q?=EC=97=85=EC=9D=B4=20=EC=A0=95=EC=83=81=EC=A0=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=85=B8=EC=B6=9C=EB=90=98=EB=8A=94=EC=A7=80=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/ui/home/screen/HomeScreen.kt | 13 +++++++++++-- .../presentation/ui/home/screen/HomeViewModel.kt | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 20a77086..f74f6d49 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sopt.clody.core.review.InAppReviewManager import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen @@ -34,6 +35,7 @@ import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme +import timber.log.Timber import java.time.LocalDate @Composable @@ -56,6 +58,7 @@ fun HomeRoute( val calendarState by homeViewModel.calendarState.collectAsStateWithLifecycle() val dailyDiariesState by homeViewModel.dailyDiariesState.collectAsStateWithLifecycle() val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() + val showInAppReviewPopup by homeViewModel.showInAppReviewPopup.collectAsStateWithLifecycle() val isError = calendarState is CalendarState.Error || dailyDiariesState is DailyDiariesState.Error val errorMessage = when { @@ -64,6 +67,12 @@ fun HomeRoute( else -> "" } + if (showInAppReviewPopup) { + InAppReviewManager.showPopup(LocalContext.current as Activity) + Timber.tag("showInAppReviewPopup").d(showInAppReviewPopup.toString()) + Timber.tag("InAppReview").d("인앱 리뷰 띄워라 !") + } + LaunchedEffect(Unit) { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) } @@ -183,7 +192,7 @@ fun HomeScreen( containerColor = ClodyTheme.colors.white, content = { innerPadding -> when (val state = calendarState) { - is CalendarState.Idle -> { } + is CalendarState.Idle -> {} is CalendarState.Loading -> { LoadingScreen() @@ -211,7 +220,7 @@ fun HomeScreen( } when (deleteDiaryState) { - is DeleteDiaryState.Idle -> { } + is DeleteDiaryState.Idle -> {} is DeleteDiaryState.Loading -> { LoadingScreen() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 86c97b0f..ea68f2a5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -2,6 +2,7 @@ package com.sopt.clody.presentation.ui.home.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.data.local.datasource.AppReviewLocalDataSource import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.util.NetworkUtil @@ -19,6 +20,7 @@ import javax.inject.Inject class HomeViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val networkUtil: NetworkUtil, + private val appReviewLocalDataSource: AppReviewLocalDataSource, ) : ViewModel() { private val _calendarState = MutableStateFlow>(CalendarState.Idle) @@ -62,6 +64,9 @@ class HomeViewModel @Inject constructor( private val _showDiaryDeleteDialog = MutableStateFlow(false) val showDiaryDeleteDialog: StateFlow get() = _showDiaryDeleteDialog + private val _showInAppReviewPopup = MutableStateFlow(appReviewLocalDataSource.shouldShowPopup) + val showInAppReviewPopup: StateFlow get() = _showInAppReviewPopup + private val _errorState = MutableStateFlow>(false to "") val errorState: StateFlow> = _errorState @@ -179,4 +184,9 @@ class HomeViewModel @Inject constructor( fun setShowDiaryDeleteDialog(state: Boolean) { _showDiaryDeleteDialog.value = state } + + fun updateShowInAppReviewPopup(state: Boolean) { + appReviewLocalDataSource.shouldShowPopup = state + _showInAppReviewPopup.value = state + } } From 890880440ecd996b1d4c3daa3908125febab323c Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 5 Jun 2025 16:26:11 +0900 Subject: [PATCH 112/299] =?UTF-8?q?[FEAT/#273]=20=EB=8B=B5=EC=9E=A5?= =?UTF-8?q?=ED=99=95=EC=9D=B8=EC=9C=BC=EB=A1=9C=EB=B6=80=ED=84=B0=20?= =?UTF-8?q?=ED=99=88=ED=99=94=EB=A9=B4=EC=9C=BC=EB=A1=9C=20=EB=8F=8C?= =?UTF-8?q?=EC=95=84=EC=99=94=EB=8A=94=20=EC=A7=80=EB=A5=BC=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=ED=95=98=EB=8A=94=20=EC=BD=94=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/home/navigation/HomeNavigation.kt | 4 +++- .../sopt/clody/presentation/ui/home/screen/HomeScreen.kt | 9 ++++++--- .../clody/presentation/ui/replydiary/ReplyDiaryScreen.kt | 6 +++--- .../ui/replydiary/navigation/ReplyDiaryNavigation.kt | 2 +- .../sopt/clody/presentation/utils/navigation/Route.kt | 1 + 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt index 6cc39e2d..550a570b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt @@ -28,6 +28,7 @@ fun NavGraphBuilder.homeScreen( selectedYear = selectedYear, selectedMonth = selectedMonth, selectedDay = selectedDay, + isFromReplyDiary = isFromReplyDiary, navigateToDiaryList = navigateToDiaryList, navigateToSetting = navigateToSetting, navigateToWriteDiary = navigateToWriteDiary, @@ -41,7 +42,8 @@ fun NavController.navigateToHome( selectedYear: Int = LocalDate.now().year, selectedMonth: Int = LocalDate.now().monthValue, selectedDay: Int? = LocalDate.now().dayOfMonth, + isFromReplyDiary: Boolean = false, navOptions: NavOptionsBuilder.() -> Unit = {}, ) { - navigate(Route.Home(selectedYear, selectedMonth, selectedDay), navOptions) + navigate(Route.Home(selectedYear, selectedMonth, selectedDay, isFromReplyDiary), navOptions) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index f74f6d49..6f4574c1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -43,6 +43,7 @@ fun HomeRoute( selectedYear: Int, selectedMonth: Int, selectedDay: Int?, + isFromReplyDiary: Boolean, navigateToDiaryList: (year: Int, month: Int) -> Unit, navigateToSetting: () -> Unit, navigateToWriteDiary: (year: Int, month: Int, date: Int) -> Unit, @@ -67,10 +68,12 @@ fun HomeRoute( else -> "" } - if (showInAppReviewPopup) { + Timber.tag("showInAppReviewPopup").d(showInAppReviewPopup.toString()) + Timber.tag("isFromReplyDiary").d(isFromReplyDiary.toString()) + + if (showInAppReviewPopup && isFromReplyDiary) { InAppReviewManager.showPopup(LocalContext.current as Activity) - Timber.tag("showInAppReviewPopup").d(showInAppReviewPopup.toString()) - Timber.tag("InAppReview").d("인앱 리뷰 띄워라 !") + homeViewModel.updateShowInAppReviewPopup(false) } LaunchedEffect(Unit) { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt index c4aa168e..f112fc19 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt @@ -49,7 +49,7 @@ fun ReplyDiaryRoute( month: Int, date: Int, replyStatus: ReplyStatus, - navigateToHome: (year: Int, month: Int, date: Int) -> Unit, + navigateToHome: (year: Int, month: Int, date: Int, isFromReplyDiary: Boolean) -> Unit, viewModel: ReplyDiaryViewModel = hiltViewModel(), ) { val replyDiaryState by viewModel.replyDiaryState.collectAsState() @@ -65,7 +65,7 @@ fun ReplyDiaryRoute( BackHandler { val currentTime = System.currentTimeMillis() if (currentTime - backPressedTime <= backPressThreshold) { - navigateToHome(year, month, date) + navigateToHome(year, month, date, true) } else { backPressedTime = currentTime } @@ -79,7 +79,7 @@ fun ReplyDiaryRoute( is ReplyDiaryState.Success -> { val successState = replyDiaryState as ReplyDiaryState.Success ReplyDiaryScreen( - navigateToHome = { navigateToHome(year, month, date) }, + navigateToHome = { navigateToHome(year, month, date, true) }, replyStatus = replyStatus, replyDiaryState = successState, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt index f1e5b161..492ea5f2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt @@ -10,7 +10,7 @@ import com.sopt.clody.presentation.ui.replydiary.ReplyDiaryRoute import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.replyDiaryScreen( - navigateToHome: (year: Int, month: Int, date: Int) -> Unit, + navigateToHome: (year: Int, month: Int, date: Int, isFromReplyDiary: Boolean) -> Unit, ) { composable { backStackEntry -> backStackEntry.toRoute().apply { diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt index 7c423e1e..334475b6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt @@ -31,6 +31,7 @@ sealed interface Route { val selectedYear: Int, val selectedMonth: Int, val selectedDay: Int? = null, + val isFromReplyDiary: Boolean = false, ) : Route @Serializable From 7eb6053ced284c3a176dea8818543677613e02cc Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 5 Jun 2025 16:29:00 +0900 Subject: [PATCH 113/299] =?UTF-8?q?[REFACTOR/#273]=20LaunchedEffect=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD,=20context=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EC=95=88=EC=A0=95=EC=84=B1=20=EC=A6=9D=EA=B0=80,=20Timber=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=EB=A5=BC=20=EC=88=98=ED=96=89=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/presentation/ui/home/screen/HomeScreen.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 6f4574c1..8e05be83 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -60,6 +60,7 @@ fun HomeRoute( val dailyDiariesState by homeViewModel.dailyDiariesState.collectAsStateWithLifecycle() val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() val showInAppReviewPopup by homeViewModel.showInAppReviewPopup.collectAsStateWithLifecycle() + val context = LocalContext.current val isError = calendarState is CalendarState.Error || dailyDiariesState is DailyDiariesState.Error val errorMessage = when { @@ -68,11 +69,8 @@ fun HomeRoute( else -> "" } - Timber.tag("showInAppReviewPopup").d(showInAppReviewPopup.toString()) - Timber.tag("isFromReplyDiary").d(isFromReplyDiary.toString()) - - if (showInAppReviewPopup && isFromReplyDiary) { - InAppReviewManager.showPopup(LocalContext.current as Activity) + LaunchedEffect(showInAppReviewPopup && isFromReplyDiary) { + InAppReviewManager.showPopup(context as Activity) homeViewModel.updateShowInAppReviewPopup(false) } From da8f6c4342543c864c69b02c2a18a4686bf97748 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 5 Jun 2025 16:32:36 +0900 Subject: [PATCH 114/299] =?UTF-8?q?[REFACTOR/#273]=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=ED=8C=9D=EC=97=85=20=EB=85=B8=EC=B6=9C=EC=97=90=20=EA=B4=80?= =?UTF-8?q?=ED=95=9C=20=EC=97=90=EB=9F=AC=20=EB=8C=80=EC=9D=91=EC=9D=84=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/core/review/InAppReviewManager.kt | 21 +++++++++++++++---- .../presentation/ui/home/screen/HomeScreen.kt | 1 - 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt b/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt index 960d611c..51254cde 100644 --- a/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt +++ b/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt @@ -4,9 +4,12 @@ import android.app.Activity import android.content.Intent import android.net.Uri import com.google.android.play.core.review.ReviewManagerFactory +import timber.log.Timber object InAppReviewManager { fun showPopup(activity: Activity) { + if (activity.isFinishing || activity.isDestroyed) return + val reviewManager = ReviewManagerFactory.create(activity) val request = reviewManager.requestReviewFlow() @@ -15,10 +18,20 @@ object InAppReviewManager { val reviewInfo = task.result reviewManager.launchReviewFlow(activity, reviewInfo) } else { - // fallback logic: 예를 들어 마켓 링크 열기 - val uri = Uri.parse("market://details?id=${activity.packageName}") - val intent = Intent(Intent.ACTION_VIEW, uri) - activity.startActivity(intent) + try { + val uri = Uri.parse("market://details?id=${activity.packageName}") + val intent = Intent(Intent.ACTION_VIEW, uri) + if (intent.resolveActivity(activity.packageManager) != null) { + activity.startActivity(intent) + } else { + val webUri = Uri.parse("https://play.google.com/store/apps/details?id=${activity.packageName}") + val webIntent = Intent(Intent.ACTION_VIEW, webUri) + activity.startActivity(webIntent) + } + } catch (e: Exception) { + e.printStackTrace() + Timber.e(e, "Failed to open store for app review") + } } } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 8e05be83..448918d1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -35,7 +35,6 @@ import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme -import timber.log.Timber import java.time.LocalDate @Composable From 29d1f2d02b90b6df38eed0deb84a40b129cd4a99 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Thu, 5 Jun 2025 17:50:49 +0900 Subject: [PATCH 115/299] =?UTF-8?q?[REFACTOR/#265]=20onDismiss,=20onDismis?= =?UTF-8?q?sExitDialog=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20BackHandler=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/component/dialog/ClodyDialog.kt | 14 ++++++------- .../ui/writediary/screen/WriteDiaryScreen.kt | 20 ++++++++++++++++++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/ClodyDialog.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/ClodyDialog.kt index 6d45a195..c0846c94 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/ClodyDialog.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/ClodyDialog.kt @@ -43,6 +43,7 @@ fun ClodyDialog( confirmButtonColor: Color, confirmButtonTextColor: Color, onDismiss: () -> Unit, + onDismissButtonClick: (() -> Unit)? = null, ) { var isButtonClicked by remember { mutableStateOf(false) } @@ -68,8 +69,7 @@ fun ClodyDialog( .wrapContentHeight(), ) { Column( - modifier = Modifier - .padding(28.dp), + modifier = Modifier.padding(28.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { @@ -92,21 +92,21 @@ fun ClodyDialog( Spacer(modifier = Modifier.height(32.dp)) Row( - modifier = Modifier - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), ) { Button( onClick = { if (!isButtonClicked) { isButtonClicked = true - onDismiss() + // dismiss 버튼 클릭 시: 우선순위는 onDismissButtonClick + onDismissButtonClick?.invoke() ?: onDismiss() } }, modifier = Modifier .weight(1f) .background( color = ClodyTheme.colors.gray07, - shape = RoundedCornerShape(size = 8.dp), + shape = RoundedCornerShape(8.dp), ), colors = ButtonDefaults.buttonColors(ClodyTheme.colors.gray07), ) { @@ -130,7 +130,7 @@ fun ClodyDialog( .weight(1f) .background( color = confirmButtonColor, - shape = RoundedCornerShape(size = 8.dp), + shape = RoundedCornerShape(8.dp), ), colors = ButtonDefaults.buttonColors(confirmButtonColor), ) { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index 073fb910..174a7a9b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -1,5 +1,6 @@ package com.sopt.clody.presentation.ui.writediary.screen +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -88,6 +89,19 @@ fun WriteDiaryRoute( } } + BackHandler { + if (showExitDialog) { + viewModel.updateShowExitDialog(false) + } else { + if (viewModel.hasChangedFromInitial()) { + AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_BACK) + viewModel.updateShowExitDialog(true) + } else { + navigateToPrevious() + } + } + } + WriteDiaryScreen( isLoading = writeDiaryState is WriteDiaryState.Loading, entries = entries, @@ -143,6 +157,7 @@ fun WriteDiaryRoute( onDismissLimitMessage = { viewModel.updateShowLimitMessage(false) }, onDismissEmptyFieldsMessage = { viewModel.updateShowEmptyFieldsMessage(false) }, onDismissFailureDialog = { viewModel.resetFailureDialog() }, + onDismiss = { viewModel.updateShowExitDialog(false) }, onDismissExitDialog = { viewModel.updateShowExitDialog(false) viewModel.saveDraftDiary() @@ -182,6 +197,7 @@ fun WriteDiaryScreen( failureMessage: String, showExitDialog: Boolean, onDismissFailureDialog: () -> Unit, + onDismiss: () -> Unit, onDismissExitDialog: () -> Unit, onConfirmExitDialog: () -> Unit, year: Int, @@ -290,7 +306,8 @@ fun WriteDiaryScreen( confirmAction = onConfirmExitDialog, confirmButtonColor = ClodyTheme.colors.red, confirmButtonTextColor = ClodyTheme.colors.white, - onDismiss = onDismissExitDialog, + onDismiss = onDismiss, + onDismissButtonClick = onDismissExitDialog, ) } } @@ -387,6 +404,7 @@ private fun WriteDiaryScreenPreview() { failureMessage = "", showExitDialog = false, onDismissFailureDialog = {}, + onDismiss = {}, onDismissExitDialog = {}, onConfirmExitDialog = {}, year = 2023, From 1863b26f2a874f4fd8bc3f85d5f7bc032f5f61f6 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Thu, 5 Jun 2025 17:51:16 +0900 Subject: [PATCH 116/299] =?UTF-8?q?[REFACTOR/#265]=20"=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EA=B8=B0"=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=EB=A9=94=EC=8B=9C=EC=A7=80=EA=B0=80=20=EB=82=98?= =?UTF-8?q?=ED=83=80=EB=82=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/writediary/component/textfield/WriteDiaryTextField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt index e3a24cde..e2593bde 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt @@ -163,7 +163,7 @@ fun WriteDiaryTextField( .fillMaxWidth() .padding(start = 8.dp, top = 6.dp), ) { - if ((showWarning && !isTextValid && text.isNotEmpty()) || isTextTooLong) { + if ((showWarning && !isTextValid) || isTextTooLong) { Text( text = "2~50자 까지 입력할 수 있어요.", color = ClodyTheme.colors.red, From 2e1090651c56baebaab74e5b149f14145544939f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Thu, 5 Jun 2025 17:51:39 +0900 Subject: [PATCH 117/299] =?UTF-8?q?[ADD/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20Fetch=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt | 7 ++++++- .../sopt/clody/presentation/utils/network/ErrorMessages.kt | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt index a32e04c9..3520efb7 100644 --- a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt @@ -91,7 +91,12 @@ class DiaryRepositoryImpl @Inject constructor( .handleApiResponse() .getOrThrow() .toDomain() - } + }.fold( + onSuccess = { Result.success(it) }, + onFailure = { + Result.failure(Exception(ErrorMessages.FETCH_TEMP_DIARY_FAILED)) + }, + ) override suspend fun saveDraftDiary(contents: List): Result = runCatching { diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt index 3aa8a1a2..3db50f7e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt @@ -4,5 +4,6 @@ object ErrorMessages { const val FAILURE_NETWORK_MESSAGE = "서비스 접속이 원활하지 않아요.\n네트워크 연결을 확인해주세요." const val FAILURE_TEMPORARY_MESSAGE = "일시적인 오류가 발생했어요.\n잠시 후 다시 시도해주세요." const val FAILURE_SERVER_MESSAGE = "서버 오류가 발생했어요.\n잠시 후 다시 시도해주세요." + const val FETCH_TEMP_DIARY_FAILED = "임시저장 불러오기에 실패했어요.\n잠시 후 다시 시도해주세요." const val UNKNOWN_ERROR = "알수없는 에러" } From 808001cfd1db93127f1c2ad78b1389de319b79e8 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Thu, 5 Jun 2025 17:51:51 +0900 Subject: [PATCH 118/299] =?UTF-8?q?[MOD/#265]=20=ED=94=84=EB=A6=B0?= =?UTF-8?q?=ED=8A=B8=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/writediary/screen/WriteDiaryViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index a99df0f5..15c24922 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -205,7 +205,6 @@ class WriteDiaryViewModel @Inject constructor( result.onSuccess { _failureMessage.value = "" _showFailureDialog.value = false - println("성공함") }.onFailure { e -> _failureMessage.value = e.localizedMessage ?: UNKNOWN_ERROR _showFailureDialog.value = true From bdeebf9c5428a8b2c52e57fb5df5c817a376fbec Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 6 Jun 2025 14:25:28 +0900 Subject: [PATCH 119/299] =?UTF-8?q?[REFACTOR/#268]=20Local/Remote=20DataSo?= =?UTF-8?q?urce=20Module=EC=9D=84=20=EB=B6=84=EB=A6=AC=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/di/LocalDataSourceModule.kt | 29 +++++++++++++++++++ ...rceModule.kt => RemoteDataSourceModule.kt} | 2 +- .../sopt/clody/di/SharedPreferencesModule.kt | 9 +----- .../com/sopt/clody/di/TokenDataStoreModule.kt | 21 -------------- 4 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/di/LocalDataSourceModule.kt rename app/src/main/java/com/sopt/clody/di/{DataSourceModule.kt => RemoteDataSourceModule.kt} (97%) delete mode 100644 app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt diff --git a/app/src/main/java/com/sopt/clody/di/LocalDataSourceModule.kt b/app/src/main/java/com/sopt/clody/di/LocalDataSourceModule.kt new file mode 100644 index 00000000..1f28e407 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/di/LocalDataSourceModule.kt @@ -0,0 +1,29 @@ +package com.sopt.clody.di + +import android.content.SharedPreferences +import com.sopt.clody.data.datastore.TokenDataStore +import com.sopt.clody.data.datastore.TokenDataStoreImpl +import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource +import com.sopt.clody.data.local.datasourceimpl.FirstDraftLocalDataSourceImpl +import com.sopt.clody.di.qualifier.FirstDraftPrefs +import com.sopt.clody.di.qualifier.TokenPrefs +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object LocalDataSourceModule { + + @Provides + @Singleton + fun provideTokenDataStore(@TokenPrefs sharedPreferences: SharedPreferences): TokenDataStore = + TokenDataStoreImpl(sharedPreferences) + + @Provides + @Singleton + fun provideFirstDraftLocalDataSource(@FirstDraftPrefs sharedPreferences: SharedPreferences): FirstDraftLocalDataSource = + FirstDraftLocalDataSourceImpl(sharedPreferences) +} diff --git a/app/src/main/java/com/sopt/clody/di/DataSourceModule.kt b/app/src/main/java/com/sopt/clody/di/RemoteDataSourceModule.kt similarity index 97% rename from app/src/main/java/com/sopt/clody/di/DataSourceModule.kt rename to app/src/main/java/com/sopt/clody/di/RemoteDataSourceModule.kt index 4d570e72..fa6da505 100644 --- a/app/src/main/java/com/sopt/clody/di/DataSourceModule.kt +++ b/app/src/main/java/com/sopt/clody/di/RemoteDataSourceModule.kt @@ -18,7 +18,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -abstract class DataSourceModule { +abstract class RemoteDataSourceModule { @Binds @Singleton abstract fun bindAuthDataSource( diff --git a/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt b/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt index 8e11e9aa..0fda0f82 100644 --- a/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt +++ b/app/src/main/java/com/sopt/clody/di/SharedPreferencesModule.kt @@ -2,8 +2,6 @@ package com.sopt.clody.di import android.content.Context import android.content.SharedPreferences -import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource -import com.sopt.clody.data.local.datasourceimpl.FirstDraftLocalDataSourceImpl import com.sopt.clody.di.qualifier.FirstDraftPrefs import com.sopt.clody.di.qualifier.TokenPrefs import dagger.Module @@ -20,7 +18,7 @@ object SharedPreferencesModule { @TokenPrefs @Provides @Singleton - fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences { + fun provideTokenSharedPreferences(@ApplicationContext context: Context): SharedPreferences { return context.getSharedPreferences("token_prefs", Context.MODE_PRIVATE) } @@ -30,9 +28,4 @@ object SharedPreferencesModule { fun provideFirstDraftSharedPreferences(@ApplicationContext context: Context): SharedPreferences { return context.getSharedPreferences("first_draft_prefs", Context.MODE_PRIVATE) } - - @Provides - @Singleton - fun provideFirstDraftLocalDataSource(@FirstDraftPrefs sharedPreferences: SharedPreferences): FirstDraftLocalDataSource = - FirstDraftLocalDataSourceImpl(sharedPreferences) } diff --git a/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt b/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt deleted file mode 100644 index 9f33f96d..00000000 --- a/app/src/main/java/com/sopt/clody/di/TokenDataStoreModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.sopt.clody.di - -import android.content.SharedPreferences -import com.sopt.clody.data.datastore.TokenDataStore -import com.sopt.clody.data.datastore.TokenDataStoreImpl -import com.sopt.clody.di.qualifier.TokenPrefs -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object TokenDataStoreModule { - @Provides - @Singleton - fun provideTokenDataStore(@TokenPrefs sharedPreferences: SharedPreferences): TokenDataStore { - return TokenDataStoreImpl(sharedPreferences) - } -} From b9d18189875f1fa7b6d33642b7d0a192e8517207 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 6 Jun 2025 14:28:18 +0900 Subject: [PATCH 120/299] =?UTF-8?q?[REFACTOR/#268]=20FcmTokenProvider?= =?UTF-8?q?=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/home/screen/HomeViewModel.kt | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 91c05c3c..36ae40c4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -3,7 +3,7 @@ package com.sopt.clody.presentation.ui.home.screen import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sopt.clody.ClodyFirebaseMessagingService.Companion.getTokenFromPreferences +import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto @@ -28,6 +28,7 @@ class HomeViewModel @Inject constructor( private val notificationRepository: NotificationRepository, private val networkUtil: NetworkUtil, private val firstDraftLocalDataSource: FirstDraftLocalDataSource, + private val fcmTokenProvider: FcmTokenProvider, ) : ViewModel() { private val _calendarState = MutableStateFlow>(CalendarState.Idle) @@ -210,22 +211,13 @@ class HomeViewModel @Inject constructor( return@launch } - val fcmToken = getFcmToken(context) ?: return@launch + val fcmToken = fcmTokenProvider.getToken().orEmpty() val notificationInfo = getNotificationInfo() ?: return@launch val request = buildDraftAlarmRequest(notificationInfo, fcmToken) sendDraftAlarmRequest(request) } } - private suspend fun getFcmToken(context: Context): String? { - val token = getTokenFromPreferences(context) - if (token.isNullOrBlank()) { - _draftAlarmChangeState.value = NotificationChangeState.Failure("FCM Token을 가져오는데 실패했습니다.") - return null - } - return token - } - private suspend fun getNotificationInfo(): NotificationInfoResponseDto? { return notificationRepository.getNotificationInfo().getOrElse { _draftAlarmChangeState.value = NotificationChangeState.Failure("알림 정보를 가져오는데 실패했습니다.") From 79c9a3ea792447a6762e9b05614c06c4918245cb Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 6 Jun 2025 14:42:15 +0900 Subject: [PATCH 121/299] =?UTF-8?q?[CHORE/#268]=20SharedPreferences?= =?UTF-8?q?=EC=9D=98=20property=20setter=EB=A5=BC=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../local/datasourceimpl/FirstDraftLocalDataSourceImpl.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/FirstDraftLocalDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/FirstDraftLocalDataSourceImpl.kt index c26a81d9..2ada28ae 100644 --- a/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/FirstDraftLocalDataSourceImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/FirstDraftLocalDataSourceImpl.kt @@ -1,6 +1,7 @@ package com.sopt.clody.data.local.datasourceimpl import android.content.SharedPreferences +import androidx.core.content.edit import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource import com.sopt.clody.di.qualifier.FirstDraftPrefs import javax.inject.Inject @@ -10,11 +11,11 @@ class FirstDraftLocalDataSourceImpl @Inject constructor( ) : FirstDraftLocalDataSource { override var isDraftUsed: Boolean get() = sharedPreferences.getBoolean(IS_DRAFT_USED, false) - set(value) = sharedPreferences.edit().putBoolean(IS_DRAFT_USED, value).apply() + set(value) = sharedPreferences.edit { putBoolean(IS_DRAFT_USED, value) } override var isFirstUse: Boolean get() = sharedPreferences.getBoolean(IS_FIRST_USE, false) - set(value) = sharedPreferences.edit().putBoolean(IS_FIRST_USE, value).apply() + set(value) = sharedPreferences.edit { putBoolean(IS_FIRST_USE, value) } companion object { private const val IS_DRAFT_USED = "IS_DRAFT_USED" From 02cffb491aad022cfef61b323b433dda967b14c6 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 6 Jun 2025 15:06:16 +0900 Subject: [PATCH 122/299] =?UTF-8?q?[CHORE/#273]=20SharedPreferences?= =?UTF-8?q?=EC=9D=98=20property=20setter=EB=A5=BC=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt index 3d20c1c3..684eb9f6 100644 --- a/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/local/datasourceimpl/AppReviewLocalDataSourceImpl.kt @@ -1,6 +1,7 @@ package com.sopt.clody.data.local.datasourceimpl import android.content.SharedPreferences +import androidx.core.content.edit import com.sopt.clody.data.local.datasource.AppReviewLocalDataSource import com.sopt.clody.di.qualifier.ReviewPrefs import javax.inject.Inject @@ -11,7 +12,7 @@ class AppReviewLocalDataSourceImpl @Inject constructor( override var shouldShowPopup: Boolean get() = sharedPreferences.getBoolean(SHOULD_SHOW_POPUP, true) - set(value) = sharedPreferences.edit().putBoolean(SHOULD_SHOW_POPUP, value).apply() + set(value) = sharedPreferences.edit { putBoolean(SHOULD_SHOW_POPUP, value) } companion object { private const val SHOULD_SHOW_POPUP = "shouldShowPopup" From 794e940347ff3f1d2e34ea97da8a6ad7bdc96aef Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Fri, 6 Jun 2025 16:47:02 +0900 Subject: [PATCH 123/299] =?UTF-8?q?[REFACTOR/#265]=20API=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20=EB=A1=9C=EC=A7=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 --- .../clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt | 1 + .../com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt | 4 ++-- .../java/com/sopt/clody/domain/repository/DiaryRepository.kt | 2 +- .../com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt index 63b1bf84..0fc706af 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/request/SaveDraftDiaryRequestDto.kt @@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable @Serializable data class SaveDraftDiaryRequestDto( + @SerialName("date") val date: String, @SerialName("draftDiaries") val draftDiaries: List, ) diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt index 3520efb7..9aa2a9d1 100644 --- a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt @@ -98,10 +98,10 @@ class DiaryRepositoryImpl @Inject constructor( }, ) - override suspend fun saveDraftDiary(contents: List): Result = + override suspend fun saveDraftDiary(date: String, contents: List): Result = runCatching { diaryRemoteDataSource - .saveDraftDiary(SaveDraftDiaryRequestDto(draftDiaries = contents)) + .saveDraftDiary(SaveDraftDiaryRequestDto(date = date, draftDiaries = contents)) .handleApiResponse() .getOrThrow() .toDomain() diff --git a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt index 486f52ba..3f2aa54b 100644 --- a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt +++ b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt @@ -18,5 +18,5 @@ interface DiaryRepository { suspend fun getMonthlyDiary(year: Int, month: Int): Result suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result - suspend fun saveDraftDiary(contents: List): Result + suspend fun saveDraftDiary(date: String, contents: List): Result } diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt index 8a66fee0..064f55a2 100644 --- a/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt +++ b/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt @@ -7,7 +7,7 @@ import javax.inject.Inject class SaveDraftDiaryUseCase @Inject constructor( private val diaryRepository: DiaryRepository, ) { - suspend operator fun invoke(contents: List): Result { - return diaryRepository.saveDraftDiary(contents) + suspend operator fun invoke(date: String, contents: List): Result { + return diaryRepository.saveDraftDiary(date, contents) } } From b6c76f4545c74d8cf0832258961b2464cb1b9a72 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Fri, 6 Jun 2025 16:47:51 +0900 Subject: [PATCH 124/299] =?UTF-8?q?[REFACTOR/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=B3=B8=20=ED=95=AD=EB=AA=A9=20=EB=B3=B4?= =?UTF-8?q?=EC=9E=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../writediary/screen/WriteDiaryViewModel.kt | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index 15c24922..cb433095 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -181,27 +181,30 @@ class WriteDiaryViewModel @Inject constructor( fun fetchDraftDiary(year: Int, month: Int, day: Int) { viewModelScope.launch { + _entries.clear() + _showWarnings.clear() + val result = fetchDraftDiaryUseCase(year, month, day) result.onSuccess { response -> - _entries.clear() - _entries.addAll(response.draftDiaries) - - initialEntries = response.draftDiaries.toList() + val drafts = response.draftDiaries.ifEmpty { listOf("") } + _entries.addAll(drafts) + initialEntries = drafts.toList() - _showWarnings.clear() _showWarnings.addAll(List(_entries.size) { false }) checkLimitMessage() checkEmptyFieldsMessage() }.onFailure { + ensureDefaultEntry() _failureMessage.value = it.localizedMessage ?: UNKNOWN_ERROR _showFailureDialog.value = true } } } - fun saveDraftDiary() { + fun saveDraftDiary(year: Int, month: Int, day: Int) { viewModelScope.launch { - val result = saveDraftDiaryUseCase(_entries.toList()) + val date = String.format("%04d-%02d-%02d", year, month, day) + val result = saveDraftDiaryUseCase(date, _entries.toList()) result.onSuccess { _failureMessage.value = "" _showFailureDialog.value = false @@ -212,6 +215,15 @@ class WriteDiaryViewModel @Inject constructor( } } + private fun ensureDefaultEntry() { + _entries.clear() + _entries.add("") + _showWarnings.clear() + _showWarnings.add(false) + checkLimitMessage() + checkEmptyFieldsMessage() + } + companion object { const val MAX_ENTRIES = 5 const val ENTRY_REGEX = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣\\W]{2,50}$" From ca66f7af664499532df677f9f01163c1789456a6 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Fri, 6 Jun 2025 16:47:53 +0900 Subject: [PATCH 125/299] =?UTF-8?q?[REFACTOR/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=B3=B8=20=ED=95=AD=EB=AA=A9=20=EB=B3=B4?= =?UTF-8?q?=EC=9E=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index 174a7a9b..7920b13e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -160,7 +160,7 @@ fun WriteDiaryRoute( onDismiss = { viewModel.updateShowExitDialog(false) }, onDismissExitDialog = { viewModel.updateShowExitDialog(false) - viewModel.saveDraftDiary() + viewModel.saveDraftDiary(year, month, date) navigateToPrevious() }, onConfirmExitDialog = { From 116ac9e3593a7cefd2994d333d425c556fe6fe4c Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 6 Jun 2025 22:52:30 +0900 Subject: [PATCH 126/299] =?UTF-8?q?[Refactor/#273]=20DraftRepository?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=95=EC=9D=98=ED=95=B4=EC=84=9C=20Repository?= =?UTF-8?q?=20Pattern=EC=9D=84=20=EC=A4=80=EC=88=98=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositoryimpl/DraftRepositoryImpl.kt | 21 +++++++++++++++++++ .../com/sopt/clody/di/RepositoryModule.kt | 8 +++++++ .../domain/repository/DraftRepository.kt | 8 +++++++ .../ui/home/screen/HomeViewModel.kt | 8 +++---- .../writediary/screen/WriteDiaryViewModel.kt | 10 ++++----- 5 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/data/repositoryimpl/DraftRepositoryImpl.kt create mode 100644 app/src/main/java/com/sopt/clody/domain/repository/DraftRepository.kt diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DraftRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DraftRepositoryImpl.kt new file mode 100644 index 00000000..2f0872f1 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DraftRepositoryImpl.kt @@ -0,0 +1,21 @@ +package com.sopt.clody.data.repositoryimpl + +import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource +import com.sopt.clody.domain.repository.DraftRepository +import javax.inject.Inject + +class DraftRepositoryImpl @Inject constructor( + private val firstDraftLocalDataSource: FirstDraftLocalDataSource, +) : DraftRepository { + override fun getIsDraftUsed(): Boolean = firstDraftLocalDataSource.isDraftUsed + + override fun setIsDraftUsed(state: Boolean) { + firstDraftLocalDataSource.isDraftUsed = state + } + + override fun getIsFirstUse(): Boolean = firstDraftLocalDataSource.isFirstUse + + override fun setIsFirstUse(state: Boolean) { + firstDraftLocalDataSource.isFirstUse = state + } +} diff --git a/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt b/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt index 006a0064..b1615eb0 100644 --- a/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt +++ b/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt @@ -4,6 +4,7 @@ import com.sopt.clody.data.repositoryimpl.AccountManagementRepositoryImpl import com.sopt.clody.data.repositoryimpl.AdRepositoryImpl import com.sopt.clody.data.repositoryimpl.AuthRepositoryImpl import com.sopt.clody.data.repositoryimpl.DiaryRepositoryImpl +import com.sopt.clody.data.repositoryimpl.DraftRepositoryImpl import com.sopt.clody.data.repositoryimpl.NotificationRepositoryImpl import com.sopt.clody.data.repositoryimpl.TokenReissueRepositoryImpl import com.sopt.clody.data.repositoryimpl.TokenRepositoryImpl @@ -11,6 +12,7 @@ import com.sopt.clody.domain.repository.AccountManagementRepository import com.sopt.clody.domain.repository.AdRepository import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.DiaryRepository +import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.TokenReissueRepository import com.sopt.clody.domain.repository.TokenRepository @@ -64,4 +66,10 @@ abstract class RepositoryModule { abstract fun bindAdRepository( adRepositoryImpl: AdRepositoryImpl, ): AdRepository + + @Binds + @Singleton + abstract fun bindDraftRepository( + draftRepositoryImpl: DraftRepositoryImpl, + ): DraftRepository } diff --git a/app/src/main/java/com/sopt/clody/domain/repository/DraftRepository.kt b/app/src/main/java/com/sopt/clody/domain/repository/DraftRepository.kt new file mode 100644 index 00000000..7e193136 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/repository/DraftRepository.kt @@ -0,0 +1,8 @@ +package com.sopt.clody.domain.repository + +interface DraftRepository { + fun getIsDraftUsed(): Boolean + fun setIsDraftUsed(state: Boolean) + fun getIsFirstUse(): Boolean + fun setIsFirstUse(state: Boolean) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 36ae40c4..c90b579d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -4,13 +4,13 @@ import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.core.fcm.FcmTokenProvider -import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository +import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState @@ -27,7 +27,7 @@ class HomeViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val notificationRepository: NotificationRepository, private val networkUtil: NetworkUtil, - private val firstDraftLocalDataSource: FirstDraftLocalDataSource, + private val draftRepository: DraftRepository, private val fcmTokenProvider: FcmTokenProvider, ) : ViewModel() { @@ -72,7 +72,7 @@ class HomeViewModel @Inject constructor( private val _showDiaryDeleteDialog = MutableStateFlow(false) val showDiaryDeleteDialog: StateFlow get() = _showDiaryDeleteDialog - private val _showFirstDraftPopup = MutableStateFlow(firstDraftLocalDataSource.isFirstUse) + private val _showFirstDraftPopup = MutableStateFlow(draftRepository.getIsFirstUse()) val showFirstDraftPopup: StateFlow = _showFirstDraftPopup private val _draftAlarmChangeState = MutableStateFlow(NotificationChangeState.Idle) @@ -200,7 +200,7 @@ class HomeViewModel @Inject constructor( } fun updateFirstDraftUse(newState: Boolean) { - firstDraftLocalDataSource.isFirstUse = newState + draftRepository.setIsFirstUse(false) _showFirstDraftPopup.value = newState } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index 289c9763..c1818f07 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -7,9 +7,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sopt.clody.data.local.datasource.FirstDraftLocalDataSource import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository +import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR @@ -23,7 +23,7 @@ import javax.inject.Inject class WriteDiaryViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val networkUtil: NetworkUtil, - private val firstDraftLocalDataSource: FirstDraftLocalDataSource, + private val draftRepository: DraftRepository, ) : ViewModel() { private val _writeDiaryState = MutableStateFlow(WriteDiaryState.Idle) @@ -172,9 +172,9 @@ class WriteDiaryViewModel @Inject constructor( } fun updateDraftUsage() { - if (!firstDraftLocalDataSource.isDraftUsed) { - firstDraftLocalDataSource.isDraftUsed = true - firstDraftLocalDataSource.isFirstUse = true + if (!draftRepository.getIsDraftUsed()) { + draftRepository.setIsDraftUsed(true) + draftRepository.setIsFirstUse(true) } } From 0fa677fbd02a8de5d2d2e2537bf24ef3b0406986 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 6 Jun 2025 23:15:02 +0900 Subject: [PATCH 127/299] =?UTF-8?q?[REFACTOR/#273]=20ReviewRepository?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=95=EC=9D=98=ED=95=98=EC=97=AC=20Repository?= =?UTF-8?q?=20Pattern=EC=9D=84=20=EC=A4=80=EC=88=98=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/repositoryimpl/ReviewRepositoryImpl.kt | 15 +++++++++++++++ .../java/com/sopt/clody/di/RepositoryModule.kt | 8 ++++++++ .../clody/domain/repository/ReviewRepository.kt | 6 ++++++ .../presentation/ui/home/screen/HomeViewModel.kt | 8 ++++---- 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/data/repositoryimpl/ReviewRepositoryImpl.kt create mode 100644 app/src/main/java/com/sopt/clody/domain/repository/ReviewRepository.kt diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/ReviewRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/ReviewRepositoryImpl.kt new file mode 100644 index 00000000..7421396a --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/ReviewRepositoryImpl.kt @@ -0,0 +1,15 @@ +package com.sopt.clody.data.repositoryimpl + +import com.sopt.clody.data.local.datasource.AppReviewLocalDataSource +import com.sopt.clody.domain.repository.ReviewRepository +import javax.inject.Inject + +class ReviewRepositoryImpl @Inject constructor( + private val appReviewLocalDataSource: AppReviewLocalDataSource, +) : ReviewRepository { + override fun getShouldShowPopup(): Boolean = appReviewLocalDataSource.shouldShowPopup + + override fun setShouldShowPopup(state: Boolean) { + appReviewLocalDataSource.shouldShowPopup = state + } +} diff --git a/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt b/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt index 006a0064..899246ac 100644 --- a/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt +++ b/app/src/main/java/com/sopt/clody/di/RepositoryModule.kt @@ -5,6 +5,7 @@ import com.sopt.clody.data.repositoryimpl.AdRepositoryImpl import com.sopt.clody.data.repositoryimpl.AuthRepositoryImpl import com.sopt.clody.data.repositoryimpl.DiaryRepositoryImpl import com.sopt.clody.data.repositoryimpl.NotificationRepositoryImpl +import com.sopt.clody.data.repositoryimpl.ReviewRepositoryImpl import com.sopt.clody.data.repositoryimpl.TokenReissueRepositoryImpl import com.sopt.clody.data.repositoryimpl.TokenRepositoryImpl import com.sopt.clody.domain.repository.AccountManagementRepository @@ -12,6 +13,7 @@ import com.sopt.clody.domain.repository.AdRepository import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.NotificationRepository +import com.sopt.clody.domain.repository.ReviewRepository import com.sopt.clody.domain.repository.TokenReissueRepository import com.sopt.clody.domain.repository.TokenRepository import dagger.Binds @@ -64,4 +66,10 @@ abstract class RepositoryModule { abstract fun bindAdRepository( adRepositoryImpl: AdRepositoryImpl, ): AdRepository + + @Binds + @Singleton + abstract fun bindReviewRepository( + reviewRepositoryImpl: ReviewRepositoryImpl, + ): ReviewRepository } diff --git a/app/src/main/java/com/sopt/clody/domain/repository/ReviewRepository.kt b/app/src/main/java/com/sopt/clody/domain/repository/ReviewRepository.kt new file mode 100644 index 00000000..044a0cf5 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/repository/ReviewRepository.kt @@ -0,0 +1,6 @@ +package com.sopt.clody.domain.repository + +interface ReviewRepository { + fun getShouldShowPopup(): Boolean + fun setShouldShowPopup(state: Boolean) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index ea68f2a5..ab6cb5f6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -2,11 +2,11 @@ package com.sopt.clody.presentation.ui.home.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sopt.clody.data.local.datasource.AppReviewLocalDataSource import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository +import com.sopt.clody.domain.repository.ReviewRepository import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData import com.sopt.clody.presentation.utils.network.ErrorMessages import dagger.hilt.android.lifecycle.HiltViewModel @@ -20,7 +20,7 @@ import javax.inject.Inject class HomeViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val networkUtil: NetworkUtil, - private val appReviewLocalDataSource: AppReviewLocalDataSource, + private val reviewRepository: ReviewRepository, ) : ViewModel() { private val _calendarState = MutableStateFlow>(CalendarState.Idle) @@ -64,7 +64,7 @@ class HomeViewModel @Inject constructor( private val _showDiaryDeleteDialog = MutableStateFlow(false) val showDiaryDeleteDialog: StateFlow get() = _showDiaryDeleteDialog - private val _showInAppReviewPopup = MutableStateFlow(appReviewLocalDataSource.shouldShowPopup) + private val _showInAppReviewPopup = MutableStateFlow(reviewRepository.getShouldShowPopup()) val showInAppReviewPopup: StateFlow get() = _showInAppReviewPopup private val _errorState = MutableStateFlow>(false to "") @@ -186,7 +186,7 @@ class HomeViewModel @Inject constructor( } fun updateShowInAppReviewPopup(state: Boolean) { - appReviewLocalDataSource.shouldShowPopup = state + reviewRepository.setShouldShowPopup(state) _showInAppReviewPopup.value = state } } From 84ccb64a5ae6b12eb2ff90922601c2e9be568cd9 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 00:42:54 +0900 Subject: [PATCH 128/299] =?UTF-8?q?[REFACTOR/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20API=20=EC=9D=91=EB=8B=B5=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=EA=B2=BD(->Unit)=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/data/remote/api/DiaryService.kt | 3 +-- .../remote/datasource/DiaryRemoteDataSource.kt | 3 +-- .../datasourceimpl/DiaryRemoteDataSourceImpl.kt | 3 +-- .../dto/response/DraftDiaryCreatedResponseDto.kt | 16 ---------------- .../data/repositoryimpl/DiaryRepositoryImpl.kt | 4 +--- .../clody/domain/model/CreatedDraftDiaryInfo.kt | 5 ----- .../clody/domain/repository/DiaryRepository.kt | 3 +-- .../domain/usecase/SaveDraftDiaryUseCase.kt | 3 +-- 8 files changed, 6 insertions(+), 34 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiaryCreatedResponseDto.kt delete mode 100644 app/src/main/java/com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt diff --git a/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt b/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt index 20519223..bd8981d7 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt @@ -6,7 +6,6 @@ import com.sopt.clody.data.remote.dto.request.WriteDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.DraftDiaryCreatedResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto @@ -73,5 +72,5 @@ interface DiaryService { @POST("api/v1/draft") suspend fun saveDraftDiary( @Body request: SaveDraftDiaryRequestDto, - ): ApiResponse + ): ApiResponse } diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt index b6f740d3..26c0d844 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt @@ -5,7 +5,6 @@ import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.DraftDiaryCreatedResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto @@ -20,5 +19,5 @@ interface DiaryRemoteDataSource { suspend fun getMonthlyDiary(year: Int, month: Int): ApiResponse suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): ApiResponse - suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse + suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse } diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt index 6cebf748..f5e35d2a 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt @@ -8,7 +8,6 @@ import com.sopt.clody.data.remote.dto.request.WriteDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.DraftDiaryCreatedResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto @@ -42,6 +41,6 @@ class DiaryRemoteDataSourceImpl @Inject constructor( override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): ApiResponse = diaryService.fetchDraftDiary(year = year, month = month, date = date) - override suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse = + override suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse = diaryService.saveDraftDiary(request) } diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiaryCreatedResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiaryCreatedResponseDto.kt deleted file mode 100644 index 1c6f3d8d..00000000 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/DraftDiaryCreatedResponseDto.kt +++ /dev/null @@ -1,16 +0,0 @@ - -package com.sopt.clody.data.remote.dto.response - -import com.sopt.clody.domain.model.CreatedDraftDiaryInfo -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class DraftDiaryCreatedResponseDto( - @SerialName("createdAt") - val createdAt: String, -) { - fun toDomain() = CreatedDraftDiaryInfo( - createdAt = createdAt, - ) -} diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt index 9aa2a9d1..38c3540c 100644 --- a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt @@ -9,7 +9,6 @@ import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto import com.sopt.clody.data.remote.util.handleApiResponse -import com.sopt.clody.domain.model.CreatedDraftDiaryInfo import com.sopt.clody.domain.model.DraftDiaryContents import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.presentation.utils.network.ErrorMessages @@ -98,12 +97,11 @@ class DiaryRepositoryImpl @Inject constructor( }, ) - override suspend fun saveDraftDiary(date: String, contents: List): Result = + override suspend fun saveDraftDiary(date: String, contents: List): Result = runCatching { diaryRemoteDataSource .saveDraftDiary(SaveDraftDiaryRequestDto(date = date, draftDiaries = contents)) .handleApiResponse() .getOrThrow() - .toDomain() } } diff --git a/app/src/main/java/com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt b/app/src/main/java/com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt deleted file mode 100644 index b8ea1325..00000000 --- a/app/src/main/java/com/sopt/clody/domain/model/CreatedDraftDiaryInfo.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.sopt.clody.domain.model - -data class CreatedDraftDiaryInfo( - val createdAt: String, -) diff --git a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt index 3f2aa54b..3666ed18 100644 --- a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt +++ b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt @@ -6,7 +6,6 @@ import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto -import com.sopt.clody.domain.model.CreatedDraftDiaryInfo import com.sopt.clody.domain.model.DraftDiaryContents interface DiaryRepository { @@ -18,5 +17,5 @@ interface DiaryRepository { suspend fun getMonthlyDiary(year: Int, month: Int): Result suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result - suspend fun saveDraftDiary(date: String, contents: List): Result + suspend fun saveDraftDiary(date: String, contents: List): Result } diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt index 064f55a2..a1b2ffa6 100644 --- a/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt +++ b/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt @@ -1,13 +1,12 @@ package com.sopt.clody.domain.usecase -import com.sopt.clody.domain.model.CreatedDraftDiaryInfo import com.sopt.clody.domain.repository.DiaryRepository import javax.inject.Inject class SaveDraftDiaryUseCase @Inject constructor( private val diaryRepository: DiaryRepository, ) { - suspend operator fun invoke(date: String, contents: List): Result { + suspend operator fun invoke(date: String, contents: List): Result { return diaryRepository.saveDraftDiary(date, contents) } } From 05b2f975f5e0613d4280cc84e4ddd876ad99a623 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 00:43:02 +0900 Subject: [PATCH 129/299] =?UTF-8?q?[REFACTOR/#265]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20API=20=EC=9D=91=EB=8B=B5=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=EA=B2=BD(->Unit)=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 +- .../com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d2b8ce36..80dcf1d5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,7 +52,7 @@ android { buildTypes { debug { isMinifyEnabled = false - buildConfigField("String", "CLODY_BASE_URL", properties["clody.base.url"].toString()) + buildConfigField("String", "CLODY_BASE_URL", properties["clody.test.url"].toString()) } release { diff --git a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt index a7a6c2eb..ae5edb60 100644 --- a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt +++ b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt @@ -6,7 +6,6 @@ import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.DraftDiaryCreatedResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto @@ -15,7 +14,7 @@ import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto class FakeDiaryRemoteDataSource : DiaryRemoteDataSource { var draftDiariesResponse: ApiResponse? = null - var saveDraftResponse: ApiResponse? = null + var saveDraftResponse: ApiResponse? = null override suspend fun writeDiary(date: String, content: List): ApiResponse { throw NotImplementedError() @@ -56,7 +55,7 @@ class FakeDiaryRemoteDataSource : DiaryRemoteDataSource { override suspend fun saveDraftDiary( request: SaveDraftDiaryRequestDto, - ): ApiResponse { + ): ApiResponse { return saveDraftResponse ?: throw IllegalStateException("saveDraftResponse not set") } @@ -73,7 +72,7 @@ class FakeDiaryRemoteDataSource : DiaryRemoteDataSource { saveDraftResponse = ApiResponse( status = 201, message = "성공", - data = DraftDiaryCreatedResponseDto(createdAt), + data = Unit ) } } From a6066648859f2734e7fc5c6de7da60522e192268 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 00:46:02 +0900 Subject: [PATCH 130/299] =?UTF-8?q?[REFACTOR/#265]=20Repository=EC=9D=98?= =?UTF-8?q?=20=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=8B=A4=ED=8C=A8=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=B2=98=EB=A6=AC=20=EC=B1=85=EC=9E=84=EC=9D=84=20?= =?UTF-8?q?ViewModel=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt | 7 +------ .../ui/writediary/screen/WriteDiaryViewModel.kt | 3 ++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt index 38c3540c..febe3024 100644 --- a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt @@ -90,12 +90,7 @@ class DiaryRepositoryImpl @Inject constructor( .handleApiResponse() .getOrThrow() .toDomain() - }.fold( - onSuccess = { Result.success(it) }, - onFailure = { - Result.failure(Exception(ErrorMessages.FETCH_TEMP_DIARY_FAILED)) - }, - ) + } override suspend fun saveDraftDiary(date: String, contents: List): Result = runCatching { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index cb433095..a44c5db3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -11,6 +11,7 @@ import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.usecase.FetchDraftDiaryUseCase import com.sopt.clody.domain.usecase.SaveDraftDiaryUseCase +import com.sopt.clody.presentation.utils.network.ErrorMessages import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR @@ -195,7 +196,7 @@ class WriteDiaryViewModel @Inject constructor( checkEmptyFieldsMessage() }.onFailure { ensureDefaultEntry() - _failureMessage.value = it.localizedMessage ?: UNKNOWN_ERROR + _failureMessage.value = ErrorMessages.FETCH_TEMP_DIARY_FAILED _showFailureDialog.value = true } } From 58c726d06ce65844887e02ba9745dac0ba8dd84c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 00:46:28 +0900 Subject: [PATCH 131/299] [ADD/#265] trailing comma --- .../java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt index ae5edb60..4829638c 100644 --- a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt +++ b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt @@ -72,7 +72,7 @@ class FakeDiaryRemoteDataSource : DiaryRemoteDataSource { saveDraftResponse = ApiResponse( status = 201, message = "성공", - data = Unit + data = Unit, ) } } From 79a4ba9b0c284c2e73acfef5457f66b524b549a0 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 7 Jun 2025 01:02:42 +0900 Subject: [PATCH 132/299] =?UTF-8?q?[CHORE/#273]=20=EB=A7=88=EC=BC=93=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=ED=95=A8=EC=88=98=EB=A5=BC=20AppUpdateUti?= =?UTF-8?q?ls=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/core/review/InAppReviewManager.kt | 16 ++++------------ .../presentation/ui/home/screen/HomeScreen.kt | 6 +++++- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt b/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt index 51254cde..08b4c496 100644 --- a/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt +++ b/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt @@ -1,13 +1,13 @@ package com.sopt.clody.core.review import android.app.Activity -import android.content.Intent -import android.net.Uri +import android.content.Context import com.google.android.play.core.review.ReviewManagerFactory +import com.sopt.clody.presentation.utils.appupdate.AppUpdateUtils import timber.log.Timber object InAppReviewManager { - fun showPopup(activity: Activity) { + fun showPopup(activity: Activity, context: Context) { if (activity.isFinishing || activity.isDestroyed) return val reviewManager = ReviewManagerFactory.create(activity) @@ -19,15 +19,7 @@ object InAppReviewManager { reviewManager.launchReviewFlow(activity, reviewInfo) } else { try { - val uri = Uri.parse("market://details?id=${activity.packageName}") - val intent = Intent(Intent.ACTION_VIEW, uri) - if (intent.resolveActivity(activity.packageManager) != null) { - activity.startActivity(intent) - } else { - val webUri = Uri.parse("https://play.google.com/store/apps/details?id=${activity.packageName}") - val webIntent = Intent(Intent.ACTION_VIEW, webUri) - activity.startActivity(webIntent) - } + AppUpdateUtils.navigateToMarket(context) } catch (e: Exception) { e.printStackTrace() Timber.e(e, "Failed to open store for app review") diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 448918d1..66bb2308 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -35,6 +35,7 @@ import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme +import timber.log.Timber import java.time.LocalDate @Composable @@ -68,8 +69,11 @@ fun HomeRoute( else -> "" } + Timber.tag("showInAppReviewPopup").e(showInAppReviewPopup.toString()) + Timber.tag("isFromReplyDiary").e(isFromReplyDiary.toString()) + LaunchedEffect(showInAppReviewPopup && isFromReplyDiary) { - InAppReviewManager.showPopup(context as Activity) + InAppReviewManager.showPopup(context as Activity, context) homeViewModel.updateShowInAppReviewPopup(false) } From 1da7261315bd44eaad5c0a81ef6638b107c7c643 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 7 Jun 2025 01:09:11 +0900 Subject: [PATCH 133/299] =?UTF-8?q?[CHORE/#273]=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=A0=84=EB=8B=AC=EC=9D=84=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sopt/clody/core/review/InAppReviewManager.kt | 4 ++-- .../com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt b/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt index 08b4c496..1801b942 100644 --- a/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt +++ b/app/src/main/java/com/sopt/clody/core/review/InAppReviewManager.kt @@ -7,7 +7,7 @@ import com.sopt.clody.presentation.utils.appupdate.AppUpdateUtils import timber.log.Timber object InAppReviewManager { - fun showPopup(activity: Activity, context: Context) { + fun showPopup(activity: Activity) { if (activity.isFinishing || activity.isDestroyed) return val reviewManager = ReviewManagerFactory.create(activity) @@ -19,7 +19,7 @@ object InAppReviewManager { reviewManager.launchReviewFlow(activity, reviewInfo) } else { try { - AppUpdateUtils.navigateToMarket(context) + AppUpdateUtils.navigateToMarket(activity) } catch (e: Exception) { e.printStackTrace() Timber.e(e, "Failed to open store for app review") diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 66bb2308..09515e5f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -73,7 +73,7 @@ fun HomeRoute( Timber.tag("isFromReplyDiary").e(isFromReplyDiary.toString()) LaunchedEffect(showInAppReviewPopup && isFromReplyDiary) { - InAppReviewManager.showPopup(context as Activity, context) + InAppReviewManager.showPopup(context as Activity) homeViewModel.updateShowInAppReviewPopup(false) } From 25d06c6bb000ed1e55f4787c8ab5fee0de291859 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 7 Jun 2025 01:16:21 +0900 Subject: [PATCH 134/299] =?UTF-8?q?[CHORE/#273]=20=EC=9D=B8=EC=95=B1?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=ED=8C=9D=EC=97=85=20=EB=85=B8=EC=B6=9C=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EA=B2=80=EC=82=AC=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=9D=84=20=EA=B0=9C=EC=84=A0=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/ui/home/screen/HomeScreen.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 09515e5f..fa7f9495 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -72,13 +72,13 @@ fun HomeRoute( Timber.tag("showInAppReviewPopup").e(showInAppReviewPopup.toString()) Timber.tag("isFromReplyDiary").e(isFromReplyDiary.toString()) - LaunchedEffect(showInAppReviewPopup && isFromReplyDiary) { - InAppReviewManager.showPopup(context as Activity) - homeViewModel.updateShowInAppReviewPopup(false) - } - LaunchedEffect(Unit) { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) + + if (showInAppReviewPopup && isFromReplyDiary) { + InAppReviewManager.showPopup(context as Activity) + homeViewModel.updateShowInAppReviewPopup(false) + } } LaunchedEffect(selectedYear, selectedMonth, selectedDay) { From 9fda999c2b93a649f47e212f49d683c301d3cc3f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 14:21:26 +0900 Subject: [PATCH 135/299] =?UTF-8?q?[ADD/#265]=20close=20blacket=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 --- gradle/libs.versions.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bafaeb48..1e3c001a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -211,6 +211,7 @@ kotest = [ "kotest-runner", "kotest-assertions", "kotest-property" +] plays = [ "play-review", From bd24e15ef82e75c79fe455ede2852843eca5f03a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 14:21:50 +0900 Subject: [PATCH 136/299] =?UTF-8?q?[ADD/#265]=20import=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/writediary/screen/WriteDiaryViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index 3b9750fd..5b27cbc1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -9,10 +9,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository +import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.usecase.FetchDraftDiaryUseCase import com.sopt.clody.domain.usecase.SaveDraftDiaryUseCase import com.sopt.clody.presentation.utils.network.ErrorMessages -import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR From 976288262a5850322986163ab5e3091df1ac9148 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:19:25 +0900 Subject: [PATCH 137/299] =?UTF-8?q?[ADD/#270]=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=90=EC=85=8B=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../drawable/ic_home_draft_saved_clover.xml | 18 ++++++++++++++++++ .../ic_home_expired_written_clover.xml | 9 +++++++++ 2 files changed, 27 insertions(+) create mode 100644 app/src/main/res/drawable/ic_home_draft_saved_clover.xml create mode 100644 app/src/main/res/drawable/ic_home_expired_written_clover.xml diff --git a/app/src/main/res/drawable/ic_home_draft_saved_clover.xml b/app/src/main/res/drawable/ic_home_draft_saved_clover.xml new file mode 100644 index 00000000..5b677aeb --- /dev/null +++ b/app/src/main/res/drawable/ic_home_draft_saved_clover.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_home_expired_written_clover.xml b/app/src/main/res/drawable/ic_home_expired_written_clover.xml new file mode 100644 index 00000000..8ca7a53c --- /dev/null +++ b/app/src/main/res/drawable/ic_home_expired_written_clover.xml @@ -0,0 +1,9 @@ + + + From cab42dba15324d269453f77faadadd0381e57533 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:19:49 +0900 Subject: [PATCH 138/299] =?UTF-8?q?[REFACTOR/#270]=20=ED=81=B4=EB=A1=9C?= =?UTF-8?q?=EB=B2=84=20=EC=95=84=EC=9D=B4=EC=BD=98=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EB=8C=80=EC=9D=91=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/domain/model/ReplyStatus.kt | 5 +- .../presentation/ui/type/DiaryCloverType.kt | 49 ++++++++++++++----- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt b/app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt index bf97c6af..f1c5885f 100644 --- a/app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt +++ b/app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt @@ -4,5 +4,8 @@ import kotlinx.serialization.Serializable @Serializable enum class ReplyStatus { - UNREADY, READY_READ, READY_NOT_READ + UNREADY, READY_READ, READY_NOT_READ, HAS_DRAFT, INVALID_DRAFT; + + val isUnreadOrNotRead: Boolean + get() = this == UNREADY || this == READY_NOT_READ } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DiaryCloverType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DiaryCloverType.kt index dd162022..bb02178a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DiaryCloverType.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DiaryCloverType.kt @@ -1,29 +1,56 @@ package com.sopt.clody.presentation.ui.type +import androidx.annotation.DrawableRes import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.domain.model.ReplyStatus -enum class DiaryCloverType(val iconRes: Int) { +/** + * DiaryData를 기반으로 해당 날짜에 보여줄 클로버 아이콘 타입을 반환. + * + * - 오늘이고 일기가 없으면 👉 [TODAY_UNWRITTEN] + * - 오늘이고 일기와 읽지 않은 답장이 있으면 👉 [TODAY_WRITTEN] + * - 임시저장이 존재하면 👉 [DRAFT_SAVED] + * - 임시저장이 만료되었으면 👉 [EXPIRED_WRITTEN] + * - 일기가 있고 답장이 없거나 읽지 않았으면 👉 [UNGIVEN_CLOVER] + * - 일기 수에 따라 👉 [BOTTOM_CLOVER], [MID_CLOVER], [TOP_CLOVER] 구분 + * - 이 외의 경우 기본값 👉 [UNGIVEN_CLOVER] + */ + +enum class DiaryCloverType(@DrawableRes val iconRes: Int) { TODAY_UNWRITTEN(R.drawable.ic_home_today_unwritten_clover), TODAY_WRITTEN(R.drawable.ic_home_today_written_clover), UNGIVEN_CLOVER(R.drawable.ic_home_ungiven_clover), BOTTOM_CLOVER(R.drawable.ic_home_bottom_clover), MID_CLOVER(R.drawable.ic_home_mid_clover), TOP_CLOVER(R.drawable.ic_home_top_clover), + DRAFT_SAVED(R.drawable.ic_home_draft_saved_clover), + EXPIRED_WRITTEN(R.drawable.ic_home_expired_written_clover), ; companion object { - fun getCalendarCloverType(diaryData: MonthlyCalendarResponseDto.Diary, isToday: Boolean): DiaryCloverType { + fun getCalendarCloverType( + diaryData: MonthlyCalendarResponseDto.Diary, + isToday: Boolean, + ): DiaryCloverType { + val count = diaryData.diaryCount + val reply = diaryData.replyStatus + + val hasDiary = count > 0 + val noDiary = count == 0 + val hasDraft = reply == ReplyStatus.HAS_DRAFT + val draftExpired = reply == ReplyStatus.INVALID_DRAFT + val hasUnreadOrNoReply = reply.isUnreadOrNotRead + return when { - isToday && diaryData.diaryCount == 0 -> TODAY_UNWRITTEN - isToday && diaryData.diaryCount > 0 && - (diaryData.replyStatus == "UNREADY" || diaryData.replyStatus == "READY_NOT_READ") -> TODAY_WRITTEN - diaryData.replyStatus == "READY_NOT_READ" && diaryData.diaryCount > 0 -> UNGIVEN_CLOVER - diaryData.replyStatus == "UNREADY" && diaryData.diaryCount > 0 -> UNGIVEN_CLOVER - diaryData.diaryCount == 0 -> UNGIVEN_CLOVER - diaryData.diaryCount in 1..2 -> BOTTOM_CLOVER - diaryData.diaryCount in 3..4 -> MID_CLOVER - diaryData.diaryCount == 5 -> TOP_CLOVER + hasDraft -> DRAFT_SAVED + isToday && noDiary -> TODAY_UNWRITTEN + isToday && hasUnreadOrNoReply -> TODAY_WRITTEN + draftExpired -> EXPIRED_WRITTEN + hasDiary && hasUnreadOrNoReply -> UNGIVEN_CLOVER + count in 1..2 -> BOTTOM_CLOVER + count in 3..4 -> MID_CLOVER + count >= 5 -> TOP_CLOVER else -> UNGIVEN_CLOVER } } From d677803866e3d96a7d9a5f62eaf3b736cb9376dc Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:20:55 +0900 Subject: [PATCH 139/299] =?UTF-8?q?[ADD/#270]=20Dto=EC=97=90=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EC=97=AC=EB=B6=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=EC=9D=84=20=EC=9C=84=ED=95=9C=20isDraft=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/data/remote/dto/response/DailyDiariesResponseDto.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/DailyDiariesResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/DailyDiariesResponseDto.kt index 1f995a09..10544990 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/DailyDiariesResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/DailyDiariesResponseDto.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable @Serializable data class DailyDiariesResponseDto( @SerialName("diaries") val diaries: List, + @SerialName("isDraft") val isDraft: Boolean, ) { @Serializable data class Diary( From bc3b5dd4aa79f802fa31e963a009ba9cdc966d48 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:21:57 +0900 Subject: [PATCH 140/299] =?UTF-8?q?[REFACTOR/#270]=20replyStatus=EB=A5=BC?= =?UTF-8?q?=20String=20=E2=86=92=20ReplyStatus=EB=A1=9C=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/remote/dto/response/MonthlyCalendarResponseDto.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt index 43eaaa12..7a85c9ee 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt @@ -1,5 +1,6 @@ package com.sopt.clody.data.remote.dto.response +import com.sopt.clody.domain.model.ReplyStatus import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -11,7 +12,7 @@ data class MonthlyCalendarResponseDto( @Serializable data class Diary( @SerialName("diaryCount") val diaryCount: Int, - @SerialName("replyStatus") val replyStatus: String, + @SerialName("replyStatus") val replyStatus: ReplyStatus, @SerialName("isDeleted") val isDeleted: Boolean, ) } From 90d39c3da5f43bce045fa2b5394282f84c282e44 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:22:49 +0900 Subject: [PATCH 141/299] =?UTF-8?q?[REFACTOR/#270]=20replyStatus=EB=A5=BC?= =?UTF-8?q?=20String=EC=97=90=EC=84=9C=20ReplyStatus=EB=A1=9C=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 --- .../clody/presentation/ui/home/calendar/component/DayItem.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DayItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DayItem.kt index 30e3d17b..aa618e95 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DayItem.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DayItem.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.type.DiaryCloverType import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.datetime.DayOfWeek @@ -56,7 +57,7 @@ fun DayItem( painter = painterResource(id = iconRes), contentDescription = "Diary clover icon", ) - if (diaryData.replyStatus == "READY_NOT_READ" && diaryData.diaryCount > 0) { + if (diaryData.replyStatus == ReplyStatus.READY_NOT_READ && diaryData.diaryCount > 0) { Image( painter = painterResource(id = R.drawable.ic_home_unread_reply), contentDescription = "Unread replies icon", From dfe2a07ea27d8fa0d2f1a7849f0830ce5caed6ae Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:23:50 +0900 Subject: [PATCH 142/299] =?UTF-8?q?[REFACTOR/#270]=20DailyDiaryListItem?= =?UTF-8?q?=EC=97=90=EC=84=9C=20DailyDiariesResponseDto=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=EB=A5=BC=20=EB=B0=9B=EC=95=84=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=83=81=ED=83=9C=20=EC=B2=98=EB=A6=AC=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 --- .../calendar/component/DailyDiaryListItem.kt | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt index cf428318..963a3af0 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt @@ -30,7 +30,7 @@ import java.time.LocalDate fun DailyDiaryListItem( date: LocalDate, dayOfWeek: DayOfWeek, - dailyDiaries: List, + dailyDiary: DailyDiariesResponseDto, onShowDiaryDeleteStateChange: (Boolean) -> Unit, ) { Column( @@ -66,26 +66,44 @@ fun DailyDiaryListItem( .clickable(onClick = { onShowDiaryDeleteStateChange(true) }), ) } - if (dailyDiaries.isEmpty()) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxSize() - .padding(vertical = 44.dp), - ) { - Text( - text = "아직 감사 일기가 없어요!", - style = ClodyTheme.typography.body3Regular, - color = ClodyTheme.colors.gray05, - textAlign = TextAlign.Center, - ) + + when { + dailyDiary.isDraft -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .padding(vertical = 44.dp), + ) { + Text( + text = "임시저장된 일기가 있어요.", + style = ClodyTheme.typography.body3Regular, + color = ClodyTheme.colors.gray05, + textAlign = TextAlign.Center, + ) + } } - } else { - dailyDiaries.forEachIndexed { index, diary -> - DiaryItem( - index = index + 1, - text = diary.content, - ) + + dailyDiary.diaries.isEmpty() -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .padding(vertical = 44.dp), + ) { + Text( + text = "아직 감사 일기가 없어요!", + style = ClodyTheme.typography.body3Regular, + color = ClodyTheme.colors.gray05, + textAlign = TextAlign.Center, + ) + } + } + + else -> { + dailyDiary.diaries.forEachIndexed { index, diary -> + DiaryItem(index = index + 1, text = diary.content) + } } } } From 40525ca379094d595b8b14b17914622631478af1 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:24:00 +0900 Subject: [PATCH 143/299] =?UTF-8?q?[REFACTOR/#270]=20DailyDiaryListItem?= =?UTF-8?q?=EC=97=90=EC=84=9C=20dailyDiaries=EB=A5=BC=20state.data?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt index 6b2888e5..3ce9cd36 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt @@ -72,7 +72,7 @@ fun ClodyCalendar( DailyDiaryListItem( date = selectedDate, dayOfWeek = initialDayOfWeek, - dailyDiaries = state.data.diaries, + dailyDiary = state.data, onShowDiaryDeleteStateChange = onShowDiaryDeleteStateChange, ) } From 43571906b2c6baa53c0bdb93bc6ddee200773f78 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:25:39 +0900 Subject: [PATCH 144/299] =?UTF-8?q?[REFACTOR/#270]=20DiaryStateButton?= =?UTF-8?q?=EC=9D=98=20=EC=A1=B0=EA=B1=B4=20=EB=B6=84=EA=B8=B0=EB=A5=BC=20?= =?UTF-8?q?canWrite/canReply=EB=A1=9C=20=EB=8B=A8=EC=88=9C=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/component/DiaryStateButton.kt | 62 ++++++++----------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt index 754f7450..00714df6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt @@ -7,68 +7,56 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.button.ClodyReplyButton -import java.time.LocalDate @Composable fun DiaryStateButton( - diaryCount: Int, - isDeleted: Boolean, + hasDraft: Boolean, + canWrite: Boolean, + canReply: Boolean, year: Int, month: Int, day: Int, onClickWriteDiary: (Int, Int, Int) -> Unit, onClickReplyDiary: () -> Unit, ) { - val today = LocalDate.now() - val isAvailableDay = year == today.year && month == today.monthValue && (day == today.dayOfMonth || day == today.dayOfMonth - 1) - - val writeDiaryEnabled = diaryCount == 0 && isAvailableDay - val writeDiaryDisabled = diaryCount == 0 && !isAvailableDay - val checkReplyEnabled = diaryCount != 0 && !isDeleted - val checkReplyDisabled = diaryCount != 0 && isDeleted + val modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) when { - writeDiaryEnabled -> { + hasDraft -> { ClodyButton( onClick = { onClickWriteDiary(year, month, day) }, - text = "일기쓰기", + text = "이어쓰기", enabled = true, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = modifier, ) } - writeDiaryDisabled -> { - ClodyButton( - onClick = { onClickWriteDiary(year, month, day) }, - text = "일기쓰기", - enabled = false, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - } - - checkReplyEnabled -> { + canReply -> { ClodyReplyButton( onClick = onClickReplyDiary, text = "답장확인", enabled = true, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = modifier, ) } - checkReplyDisabled -> { - ClodyReplyButton( - onClick = onClickReplyDiary, - text = "답장확인", + canWrite -> { + ClodyButton( + onClick = { onClickWriteDiary(year, month, day) }, + text = "일기쓰기", + enabled = true, + modifier = modifier, + ) + } + + else -> { + ClodyButton( + onClick = { onClickWriteDiary(year, month, day) }, + text = "일기쓰기", enabled = false, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = modifier, ) } } From 493b33ce976a2790a4c0474b0880038da1902792 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:29:09 +0900 Subject: [PATCH 145/299] =?UTF-8?q?[REFACTOR/#270]=20replyStatus=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=20=EB=B0=A9=EC=8B=9D=EC=9D=84=20String=20?= =?UTF-8?q?=E2=86=92=20ReplyStatus=20enum=EC=9C=BC=EB=A1=9C=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 --- .../presentation/ui/home/screen/ScrollableCalendar.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/ScrollableCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/ScrollableCalendar.kt index 1c8ff47b..920c1616 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/ScrollableCalendar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/ScrollableCalendar.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.home.calendar.ClodyCalendar import com.sopt.clody.presentation.ui.home.component.CloverCount import com.sopt.clody.ui.theme.ClodyTheme @@ -26,13 +27,16 @@ fun ScrollableCalendar( diaries: List, onShowDiaryDeleteStateChange: (Boolean) -> Unit, selectedDate: LocalDate, - onDiaryDataUpdated: (Int, String) -> Unit, + onDiaryDataUpdated: (Int, ReplyStatus) -> Unit, modifier: Modifier = Modifier, ) { LaunchedEffect(selectedDate, diaries) { if (selectedDate.year == selectedYear && selectedDate.monthValue == selectedMonth) { homeViewModel.updateDiaryState(diaries) - onDiaryDataUpdated(homeViewModel.diaryCount.value, homeViewModel.replyStatus.value) + onDiaryDataUpdated( + homeViewModel.diaryCount.value, + homeViewModel.replyStatus.value, + ) } } val scrollState = rememberScrollState() From 69dd3965b51ebc7a4ae897fba4c5208ef31637ae Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:32:12 +0900 Subject: [PATCH 146/299] =?UTF-8?q?[REFACTOR/#270]=20ReplyStatus=20enum=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replyStatus 상태를 String → ReplyStatus enum으로 변경하여 타입 안정성 향상 - 일기 상태 판별 로직(canWriteDiary, canReplyDiary, isValidDraftDate) 별도 함수로 분리 - showContinueDraftDialog 상태 플래그 추가 - deleteDiary 시 상태 초기화 로직 추가 (diaryCount, isDeleted, replyStatus) --- .../ui/home/screen/HomeViewModel.kt | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 38b5d732..939ef899 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -9,6 +9,7 @@ import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository @@ -56,8 +57,8 @@ class HomeViewModel @Inject constructor( private val _diaryCount = MutableStateFlow(0) val diaryCount: StateFlow get() = _diaryCount - private val _replyStatus = MutableStateFlow("UNREADY") - val replyStatus: StateFlow get() = _replyStatus + private val _replyStatus = MutableStateFlow(ReplyStatus.UNREADY) + val replyStatus: StateFlow get() = _replyStatus private val _isToday = MutableStateFlow(false) val isToday: StateFlow get() = _isToday @@ -74,6 +75,9 @@ class HomeViewModel @Inject constructor( private val _showDiaryDeleteDialog = MutableStateFlow(false) val showDiaryDeleteDialog: StateFlow get() = _showDiaryDeleteDialog + private val _showContinueDraftDialog = MutableStateFlow(false) + val showContinueDraftDialog: StateFlow get() = _showContinueDraftDialog + private val _showFirstDraftPopup = MutableStateFlow(draftRepository.getIsFirstUse()) val showFirstDraftPopup: StateFlow = _showFirstDraftPopup @@ -159,6 +163,10 @@ class HomeViewModel @Inject constructor( onSuccess = { loadCalendarData(year, month) loadDailyDiariesData(year, month, day) + _diaryCount.value = 0 + _isDeleted.value = false + _replyStatus.value = ReplyStatus.UNREADY + DeleteDiaryState.Success }, onFailure = { @@ -169,7 +177,10 @@ class HomeViewModel @Inject constructor( } fun refreshCalendarDataCalendarData(year: Int, month: Int) { - if (calendarState.value is CalendarState.Success && _selectedDiaryDate.value.year == year && _selectedDiaryDate.value.month == month) { + if (calendarState.value is CalendarState.Success && + _selectedDiaryDate.value.year == year && + _selectedDiaryDate.value.month == month + ) { return } _selectedDiaryDate.value = DiaryDateData(year, month) @@ -188,7 +199,7 @@ class HomeViewModel @Inject constructor( fun updateDiaryState(diaries: List) { val selectedDiary = diaries.getOrNull(_selectedDate.value.dayOfMonth - 1) _diaryCount.value = selectedDiary?.diaryCount ?: 0 - _replyStatus.value = selectedDiary?.replyStatus ?: "UNREADY" + _replyStatus.value = selectedDiary?.replyStatus ?: ReplyStatus.UNREADY _isDeleted.value = selectedDiary?.isDeleted ?: false } @@ -204,11 +215,32 @@ class HomeViewModel @Inject constructor( _showDiaryDeleteDialog.value = state } + fun setShowContinueDraftDialog(state: Boolean) { + _showContinueDraftDialog.value = state + } + fun updateFirstDraftUse(newState: Boolean) { draftRepository.setIsFirstUse(false) _showFirstDraftPopup.value = newState } + fun canWriteDiary(): Boolean { + val today = LocalDate.now() + val selected = _selectedDate.value + val isAvailableDay = selected == today || selected == today.minusDays(1) + return _diaryCount.value == 0 && isAvailableDay + } + + fun canReplyDiary(): Boolean { + return _diaryCount.value > 0 && !_isDeleted.value + } + + fun isValidDraftDate(): Boolean { + val today = LocalDate.now() + val selected = _selectedDate.value + return selected == today || selected == today.minusDays(1) + } + fun enableDraftAlarm(context: Context) { viewModelScope.launch { if (!networkUtil.isNetworkAvailable()) { From 4beaac1f10bdfb3fe5595fc753842a627b1a3b1f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:34:19 +0900 Subject: [PATCH 147/299] =?UTF-8?q?[REFACTOR/#270]=20HomeRoute=20=EB=B0=8F?= =?UTF-8?q?=20HomeScreen=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20ReplyStatus=20Enum=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HomeRoute에서 selectedYear/Month/Day → selectedDiaryDate, selectedDate로 구조 간소화 - diaryCount, isDeleted → ReplyStatus 기반 분기 처리 개선 - onClickWriteDiary 내부에서 ReplyStatus 기반 이어쓰기 분기 처리 추가 - showContinueDraftDialog 처리 로직 추가 --- .../presentation/ui/home/screen/HomeScreen.kt | 219 ++++++++++-------- 1 file changed, 125 insertions(+), 94 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 6d94bffa..b0f59847 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -27,6 +27,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.clody.R import com.sopt.clody.core.review.InAppReviewManager +import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen @@ -47,9 +48,6 @@ import java.time.LocalDate @Composable fun HomeRoute( - selectedYear: Int, - selectedMonth: Int, - selectedDay: Int?, isFromReplyDiary: Boolean, navigateToDiaryList: (year: Int, month: Int) -> Unit, navigateToSetting: () -> Unit, @@ -64,19 +62,19 @@ fun HomeRoute( homeViewModel: HomeViewModel = hiltViewModel(), ) { val calendarState by homeViewModel.calendarState.collectAsStateWithLifecycle() - val dailyDiariesState by homeViewModel.dailyDiariesState.collectAsStateWithLifecycle() val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() val showFirstDraftPopup by homeViewModel.showFirstDraftPopup.collectAsStateWithLifecycle() val draftAlarmEnableToast by homeViewModel.draftAlarmEnableToast.collectAsStateWithLifecycle() val context = LocalContext.current val showInAppReviewPopup by homeViewModel.showInAppReviewPopup.collectAsStateWithLifecycle() - - val isError = calendarState is CalendarState.Error || dailyDiariesState is DailyDiariesState.Error - val errorMessage = when { - calendarState is CalendarState.Error -> (calendarState as CalendarState.Error).message - dailyDiariesState is DailyDiariesState.Error -> (dailyDiariesState as DailyDiariesState.Error).message - else -> "" - } + val showContinueDraftDialog by homeViewModel.showContinueDraftDialog.collectAsStateWithLifecycle() + val showDiaryDeleteState by homeViewModel.showDiaryDeleteState.collectAsStateWithLifecycle() + val showDiaryDeleteDialog by homeViewModel.showDiaryDeleteDialog.collectAsStateWithLifecycle() + val selectedDiaryDate by homeViewModel.selectedDiaryDate.collectAsStateWithLifecycle() + val selectedDate by homeViewModel.selectedDate.collectAsStateWithLifecycle() + val deleteDiaryState by homeViewModel.deleteDiaryState.collectAsStateWithLifecycle() + val (isError, errorMessage) = homeViewModel.errorState.collectAsStateWithLifecycle().value + val showYearMonthPickerState by homeViewModel.showYearMonthPickerState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) @@ -87,36 +85,49 @@ fun HomeRoute( } } - LaunchedEffect(selectedYear, selectedMonth, selectedDay) { - homeViewModel.refreshCalendarDataCalendarData(selectedYear, selectedMonth) + LaunchedEffect(selectedDiaryDate.year, selectedDiaryDate.month, selectedDate.dayOfMonth) { + homeViewModel.refreshCalendarDataCalendarData(selectedDiaryDate.year, selectedDiaryDate.month) - if (selectedDay != null) { - homeViewModel.updateSelectedDate(LocalDate.of(selectedYear, selectedMonth, selectedDay)) - homeViewModel.loadDailyDiariesData(selectedYear, selectedMonth, selectedDay) + if (selectedDate.dayOfMonth != 0) { + homeViewModel.updateSelectedDate( + LocalDate.of(selectedDiaryDate.year, selectedDiaryDate.month, selectedDate.dayOfMonth), + ) } else { - val today = LocalDate.now() - homeViewModel.updateSelectedDate(today) - homeViewModel.loadDailyDiariesData(today.year, today.monthValue, today.dayOfMonth) + homeViewModel.updateSelectedDate(LocalDate.now()) } } - if (isError) { FailureScreen( message = errorMessage, confirmAction = { - val selectedDate = homeViewModel.selectedDate.value - homeViewModel.refreshCalendarDataCalendarData(selectedYear, selectedMonth) - homeViewModel.loadDailyDiariesData(selectedYear, selectedMonth, selectedDate.dayOfMonth) + homeViewModel.refreshCalendarDataCalendarData( + selectedDiaryDate.year, + selectedDiaryDate.month, + ) + homeViewModel.loadDailyDiariesData( + selectedDiaryDate.year, + selectedDiaryDate.month, + selectedDate.dayOfMonth, + ) }, ) } else { HomeScreen( homeViewModel = homeViewModel, + calendarState = calendarState, + deleteDiaryState = deleteDiaryState, + showYearMonthPickerState = showYearMonthPickerState, onClickDiaryList = navigateToDiaryList, onClickSetting = navigateToSetting, onClickWriteDiary = { year, month, day -> + val hasDraft = replyStatus == ReplyStatus.HAS_DRAFT + val isValidDraftDate = homeViewModel.isValidDraftDate() AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_WRITING_DIARY) - navigateToWriteDiary(year, month, day) + if (hasDraft && isValidDraftDate) { + homeViewModel.setShowContinueDraftDialog(true) + } else { + navigateToWriteDiary(year, month, day) + } }, onClickReplyDiary = { year, month, day, _ -> AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_REPLY) @@ -125,11 +136,17 @@ fun HomeRoute( month, day, Route.ReplyLoading.ReplyLoadingFrom.HOME, - ReplyStatus.valueOf(replyStatus), + replyStatus, ) }, - selectedYear = selectedYear, - selectedMonth = selectedMonth, + isError = isError, + errorMessage = errorMessage, + selectedYear = selectedDiaryDate.year, + selectedMonth = selectedDiaryDate.month, + selectedDate = selectedDate, + hasDraft = replyStatus == ReplyStatus.HAS_DRAFT, + canWrite = homeViewModel.canWriteDiary(), + canReply = homeViewModel.canReplyDiary(), ) if (showFirstDraftPopup) { @@ -197,12 +214,67 @@ fun HomeRoute( onDismiss = { homeViewModel.resetDraftAlarmEnableToast() }, ) } + + if (showContinueDraftDialog) { + ClodyDialog( + titleMassage = "임시저장된 일기를 이어 쓸까요?", + descriptionMassage = "답장 기한이 지나서 답장은 받을 수 없어요.", + confirmOption = "이어쓰기", + dismissOption = "아니오", + confirmAction = { + homeViewModel.setShowContinueDraftDialog(false) + val date = homeViewModel.selectedDate.value + navigateToWriteDiary(date.year, date.monthValue, date.dayOfMonth) + }, + onDismiss = { + homeViewModel.setShowContinueDraftDialog(false) + }, + confirmButtonColor = ClodyTheme.colors.mainYellow, + confirmButtonTextColor = ClodyTheme.colors.gray01, + ) + } + + if (showDiaryDeleteState) { + DiaryDeleteSheet( + onDismiss = { homeViewModel.setShowDiaryDeleteState(false) }, + showDiaryDeleteDialog = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_DELETE_DIARY) + homeViewModel.setShowDiaryDeleteDialog(true) + }, + ) + } + + if (showDiaryDeleteDialog) { + ClodyDialog( + titleMassage = "정말 일기를 삭제할까요?", + descriptionMassage = "아직 답장이 오지 않았거나 삭제하고\n다시 작성한 일기는 답장을 받을 수 없어요.", + confirmOption = "삭제할래요", + dismissOption = "아니요", + confirmAction = { + homeViewModel.deleteDailyDiary( + selectedDiaryDate.year, + selectedDiaryDate.month, + selectedDate.dayOfMonth, + ) + homeViewModel.setShowDiaryDeleteDialog(false) + }, + onDismiss = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_NO_DELETE_DIARY) + homeViewModel.setShowDiaryDeleteDialog(false) + }, + confirmButtonColor = ClodyTheme.colors.red, + confirmButtonTextColor = ClodyTheme.colors.white, + ) + } } } @Composable fun HomeScreen( homeViewModel: HomeViewModel, + calendarState: CalendarState, + deleteDiaryState: DeleteDiaryState, + showYearMonthPickerState: Boolean, onClickDiaryList: (Int, Int) -> Unit, onClickSetting: () -> Unit, onClickWriteDiary: (Int, Int, Int) -> Unit, @@ -212,33 +284,24 @@ fun HomeScreen( date: Int, replyStatus: Route.ReplyLoading.ReplyLoadingFrom, ) -> Unit, + isError: Boolean, + errorMessage: String, selectedYear: Int, selectedMonth: Int, + selectedDate: LocalDate, + hasDraft: Boolean, + canWrite: Boolean, + canReply: Boolean, ) { - val (isError, errorMessage) = homeViewModel.errorState.collectAsStateWithLifecycle().value - if (isError) { FailureScreen( message = errorMessage, confirmAction = { homeViewModel.refreshCalendarDataCalendarData(selectedYear, selectedMonth) - val selectedDate = homeViewModel.selectedDate.value homeViewModel.loadDailyDiariesData(selectedYear, selectedMonth, selectedDate.dayOfMonth) }, ) } else { - val selectedDiaryDate by homeViewModel.selectedDiaryDate.collectAsStateWithLifecycle() - val selectedDate by homeViewModel.selectedDate.collectAsStateWithLifecycle() - val diaryCount by homeViewModel.diaryCount.collectAsStateWithLifecycle() - val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() - val isToday by homeViewModel.isToday.collectAsStateWithLifecycle() - val isDeleted by homeViewModel.isDeleted.collectAsStateWithLifecycle() - val calendarState by homeViewModel.calendarState.collectAsStateWithLifecycle() - val deleteDiaryState by homeViewModel.deleteDiaryState.collectAsStateWithLifecycle() - val showYearMonthPickerState by homeViewModel.showYearMonthPickerState.collectAsStateWithLifecycle() - val showDiaryDeleteState by homeViewModel.showDiaryDeleteState.collectAsStateWithLifecycle() - val showDiaryDeleteDialog by homeViewModel.showDiaryDeleteDialog.collectAsStateWithLifecycle() - var backPressedTime by remember { mutableStateOf(0L) } val backPressThreshold = 2000 val context = LocalContext.current @@ -257,17 +320,17 @@ fun HomeScreen( HomeTopAppBar( onClickDiaryList = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_LIST_DIARY) - onClickDiaryList(selectedDiaryDate.year, selectedDiaryDate.month) + onClickDiaryList(selectedYear, selectedMonth) }, onClickSetting = onClickSetting, onShowYearMonthPickerStateChange = { newState -> homeViewModel.setShowYearMonthPickerState(newState) }, - selectedYear = selectedDiaryDate.year, - selectedMonth = selectedDiaryDate.month, + selectedYear = selectedYear, + selectedMonth = selectedMonth, ) }, containerColor = ClodyTheme.colors.white, content = { innerPadding -> - when (val state = calendarState) { + when (calendarState) { is CalendarState.Idle -> {} is CalendarState.Loading -> { @@ -276,22 +339,22 @@ fun HomeScreen( is CalendarState.Success -> { ScrollableCalendar( - selectedYear = selectedDiaryDate.year, - selectedMonth = selectedDiaryDate.month, - cloverCount = state.data.totalCloverCount, - diaries = state.data.diaries, + selectedYear = selectedYear, + selectedMonth = selectedMonth, + cloverCount = calendarState.data.totalCloverCount, + diaries = calendarState.data.diaries, homeViewModel = homeViewModel, onShowDiaryDeleteStateChange = { newState -> homeViewModel.setShowDiaryDeleteState(newState) }, selectedDate = selectedDate, - onDiaryDataUpdated = { diaryCount, replyStatus -> - homeViewModel.updateDiaryState(state.data.diaries) + onDiaryDataUpdated = { _, _ -> + homeViewModel.updateDiaryState(calendarState.data.diaries) }, modifier = Modifier.padding(innerPadding), ) } is CalendarState.Error -> { - homeViewModel.setErrorState(true, state.message ?: "알 수 없는 오류가 발생했습니다.") + homeViewModel.setErrorState(true, calendarState.message ?: "알 수 없는 오류가 발생했습니다.") } } @@ -318,16 +381,17 @@ fun HomeScreen( ) { Spacer(modifier = Modifier.height(14.dp)) DiaryStateButton( - diaryCount = diaryCount, - isDeleted = isDeleted, - year = selectedDate.year, - month = selectedDate.monthValue, + hasDraft = hasDraft, + canWrite = canWrite, + canReply = canReply, + year = selectedYear, + month = selectedMonth, day = selectedDate.dayOfMonth, onClickWriteDiary = onClickWriteDiary, onClickReplyDiary = { onClickReplyDiary( - selectedDate.year, - selectedDate.monthValue, + selectedYear, + selectedMonth, selectedDate.dayOfMonth, Route.ReplyLoading.ReplyLoadingFrom.HOME, ) @@ -342,8 +406,8 @@ fun HomeScreen( ClodyPopupBottomSheet(onDismissRequest = { homeViewModel.setShowYearMonthPickerState(false) }) { YearMonthPicker( onDismissRequest = { homeViewModel.setShowYearMonthPickerState(false) }, - selectedYear = selectedDiaryDate.year, - selectedMonth = selectedDiaryDate.month, + selectedYear = selectedYear, + selectedMonth = selectedMonth, onYearMonthSelected = { year, month -> homeViewModel.updateSelectedDiaryDate(DiaryDateData(year, month)) homeViewModel.loadCalendarData(year, month) @@ -351,38 +415,5 @@ fun HomeScreen( ) } } - - if (showDiaryDeleteState) { - DiaryDeleteSheet( - onDismiss = { homeViewModel.setShowDiaryDeleteState(false) }, - showDiaryDeleteDialog = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_DELETE_DIARY) - homeViewModel.setShowDiaryDeleteDialog(true) - }, - ) - } - - if (showDiaryDeleteDialog) { - ClodyDialog( - titleMassage = "정말 일기를 삭제할까요?", - descriptionMassage = "아직 답장이 오지 않았거나 삭제하고\n다시 작성한 일기는 답장을 받을 수 없어요.", - confirmOption = "삭제할래요", - dismissOption = "아니요", - confirmAction = { - homeViewModel.deleteDailyDiary( - selectedDiaryDate.year, - selectedDiaryDate.month, - selectedDate.dayOfMonth, - ) - homeViewModel.setShowDiaryDeleteDialog(false) - }, - onDismiss = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_NO_DELETE_DIARY) - homeViewModel.setShowDiaryDeleteDialog(false) - }, - confirmButtonColor = ClodyTheme.colors.red, - confirmButtonTextColor = ClodyTheme.colors.white, - ) - } } } From 09678bc8cc86a9ccfa90c89f7db04812c725276e Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 17:34:38 +0900 Subject: [PATCH 148/299] =?UTF-8?q?[DEL/#270]=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/ui/home/navigation/HomeNavigation.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt index 550a570b..bc68535a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt @@ -25,9 +25,6 @@ fun NavGraphBuilder.homeScreen( composable { backStackEntry -> backStackEntry.toRoute().apply { HomeRoute( - selectedYear = selectedYear, - selectedMonth = selectedMonth, - selectedDay = selectedDay, isFromReplyDiary = isFromReplyDiary, navigateToDiaryList = navigateToDiaryList, navigateToSetting = navigateToSetting, From 13964a6083b2a99f093fd40b3b10a326f8d0cb1c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 18:09:49 +0900 Subject: [PATCH 149/299] =?UTF-8?q?[REFACTOR/#276]=20.trim()=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=B4=EC=84=9C=20=EC=8B=A4=EC=A0=9C=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=B9=84=EA=B5=90=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/writediary/screen/WriteDiaryViewModel.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index 5b27cbc1..a63ee74d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -179,7 +179,10 @@ class WriteDiaryViewModel @Inject constructor( } fun hasChangedFromInitial(): Boolean { - return entries != initialEntries + if (initialEntries.isEmpty()) return false + val current = entries.map { it.trim() } + val initial = initialEntries.map { it.trim() } + return current != initial } fun fetchDraftDiary(year: Int, month: Int, day: Int) { From b170393ea87cfcde8b74e56b0c804698b7e438d2 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 18:11:12 +0900 Subject: [PATCH 150/299] =?UTF-8?q?[REFACTOR/#276]=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=EC=9D=B4=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=EC=97=90=EB=A7=8C=20=EC=A2=85=EB=A3=8C=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C,=20=EB=92=A4=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EB=8A=94=20=ED=95=AD=EC=83=81=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=98=ED=82=B9=ED=95=98=EB=8F=84=EB=A1=9D=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/ui/writediary/screen/WriteDiaryScreen.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index 3af5e77e..7ce2e175 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -114,11 +114,11 @@ fun WriteDiaryRoute( failureMessage = failureMessage, showExitDialog = showExitDialog, onClickBack = { - if (viewModel.hasChangedFromInitial()) { - AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_BACK) - viewModel.updateShowExitDialog(true) - } else { + AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_BACK) + if (!viewModel.hasChangedFromInitial()) { navigateToPrevious() + } else { + viewModel.updateShowExitDialog(true) } }, onClickAdd = { From 7a5931bfc5a909bbd7460caa1d6f5d9e41019106 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 7 Jun 2025 18:11:32 +0900 Subject: [PATCH 151/299] =?UTF-8?q?[DEL/#276]=20=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/bottomsheet/DeleteWriteDiaryBottomSheet.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt index 5446dbc3..0a723e1d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt @@ -9,7 +9,6 @@ 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.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -67,7 +66,6 @@ fun DeleteWriteDiaryBottomSheet( ) } Spacer(modifier = Modifier.navigationBarsPadding()) - Spacer(modifier = Modifier.height(60.dp)) } }, ) From 817d1252ab657bc734c3d228a8ca3bc938857997 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sun, 8 Jun 2025 19:46:36 +0900 Subject: [PATCH 152/299] =?UTF-8?q?[CHORE/#278]=20=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=EC=9D=98=20=EC=9C=84=EC=B9=98=EB=A5=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/home/screen/HomeScreen.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index b0f59847..9fd38af7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -4,8 +4,10 @@ import android.app.Activity import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.navigationBarsPadding @@ -205,13 +207,22 @@ fun HomeRoute( } if (draftAlarmEnableToast) { - ClodyToastMessage( - message = "이어쓰기 알림 설정을 완료했어요.", - iconResId = R.drawable.ic_toast_check_on_18, - backgroundColor = ClodyTheme.colors.gray04, - contentColor = ClodyTheme.colors.white, - durationMillis = 3000, - onDismiss = { homeViewModel.resetDraftAlarmEnableToast() }, + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + content = { + ClodyToastMessage( + message = "이어쓰기 알림 설정을 완료했어요.", + iconResId = R.drawable.ic_toast_check_on_18, + backgroundColor = ClodyTheme.colors.gray04, + contentColor = ClodyTheme.colors.white, + durationMillis = 3000, + onDismiss = { homeViewModel.resetDraftAlarmEnableToast() }, + modifier = Modifier + .navigationBarsPadding() + .padding(40.dp), + ) + }, ) } From cc90d2f792234b9f38d8f92fdbc4a997ce79cfda Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 17:36:11 +0900 Subject: [PATCH 153/299] =?UTF-8?q?[REFACTOR/#280]=20ClodyButton=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EA=B4=80=EB=A0=A8=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=99=B8=EB=B6=80=20=EC=A3=BC=EC=9E=85=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=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 --- .../ui/component/button/ClodyButton.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/button/ClodyButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/button/ClodyButton.kt index accd545d..3fb7dea8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/button/ClodyButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/button/ClodyButton.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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 com.sopt.clody.ui.theme.ClodyTheme @@ -17,26 +18,26 @@ fun ClodyButton( text: String, enabled: Boolean, modifier: Modifier = Modifier, + containerColor: Color = if (enabled) ClodyTheme.colors.mainYellow else ClodyTheme.colors.lightYellow, + contentColor: Color = if (enabled) ClodyTheme.colors.gray01 else ClodyTheme.colors.gray06, + disabledContainerColor: Color = ClodyTheme.colors.lightYellow, + disabledContentColor: Color = ClodyTheme.colors.gray06, ) { - val backgroundColor = if (enabled) ClodyTheme.colors.mainYellow else ClodyTheme.colors.lightYellow - val contentColor = if (enabled) ClodyTheme.colors.gray01 else ClodyTheme.colors.gray06 - Button( onClick = onClick, colors = ButtonDefaults.buttonColors( - containerColor = backgroundColor, + containerColor = containerColor, contentColor = contentColor, - disabledContainerColor = ClodyTheme.colors.lightYellow, - disabledContentColor = ClodyTheme.colors.gray06, + disabledContainerColor = disabledContainerColor, + disabledContentColor = disabledContentColor, ), shape = RoundedCornerShape(10.dp), enabled = enabled, - modifier = modifier - .height(50.dp), + modifier = modifier.height(50.dp), ) { Text( text = text, - color = contentColor, + color = if (enabled) contentColor else disabledContentColor, style = ClodyTheme.typography.body2SemiBold, ) } From 6372e6b35088f536b228756beea31ac00d0d2f14 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 17:36:58 +0900 Subject: [PATCH 154/299] =?UTF-8?q?[ADD/#280]=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=ED=99=94=EB=A9=B4=20=EB=8B=B5=EC=9E=A5=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=B2=84=ED=8A=BC=EC=97=90=20Invalid=20Draft=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/diarylist/component/DailyDiaryCard.kt | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt index 5872a044..bd1d3fce 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt @@ -47,6 +47,7 @@ fun DailyDiaryCard( val iconRes = when { dailyDiary.replyStatus == ReplyStatus.READY_NOT_READ && dailyDiary.diaryCount > 0 -> R.drawable.ic_home_ungiven_clover dailyDiary.replyStatus == ReplyStatus.UNREADY && dailyDiary.diaryCount > 0 -> R.drawable.ic_home_ungiven_clover + dailyDiary.replyStatus == ReplyStatus.INVALID_DRAFT -> R.drawable.ic_home_expired_written_clover dailyDiary.diaryCount == 0 -> R.drawable.ic_home_ungiven_clover dailyDiary.diaryCount in 1..2 -> R.drawable.ic_home_bottom_clover dailyDiary.diaryCount in 3..4 -> R.drawable.ic_home_mid_clover @@ -123,6 +124,18 @@ fun ReplyDiaryButton( day: Int, onClickReplyDiary: (Int, Int, Int, ReplyStatus) -> Unit, ) { + val isDisabled = dailyDiary.isDeleted || dailyDiary.replyStatus == ReplyStatus.INVALID_DRAFT + val containerColor = if (dailyDiary.replyStatus == ReplyStatus.INVALID_DRAFT) { + ClodyTheme.colors.gray08 + } else { + ClodyTheme.colors.lightBlue + } + val contentColor = if (dailyDiary.replyStatus == ReplyStatus.INVALID_DRAFT) { + ClodyTheme.colors.gray06 + } else { + ClodyTheme.colors.blue + } + Box( contentAlignment = Alignment.TopEnd, ) { @@ -131,10 +144,10 @@ fun ReplyDiaryButton( modifier = Modifier .height(33.dp) .padding(horizontal = 3.dp, vertical = 3.dp), - enabled = !(dailyDiary.isDeleted), + enabled = !isDisabled, colors = ButtonDefaults.buttonColors( - containerColor = ClodyTheme.colors.lightBlue, - contentColor = ClodyTheme.colors.blue, + containerColor = containerColor, + contentColor = contentColor, disabledContainerColor = ClodyTheme.colors.gray08, disabledContentColor = ClodyTheme.colors.gray06, ), From fd41f1cec98b00f690cd3c81f4d13a76e8323fa2 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 17:37:32 +0900 Subject: [PATCH 155/299] =?UTF-8?q?[REFACTOR/#280]=20DiaryStateButton?= =?UTF-8?q?=EC=97=90=20Invalid=20Draft=20=EC=83=81=ED=83=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/component/DiaryStateButton.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt index 00714df6..851f5eae 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt @@ -7,12 +7,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.button.ClodyReplyButton +import com.sopt.clody.ui.theme.ClodyTheme @Composable fun DiaryStateButton( hasDraft: Boolean, canWrite: Boolean, canReply: Boolean, + isInvalidDraft: Boolean, year: Int, month: Int, day: Int, @@ -33,6 +35,17 @@ fun DiaryStateButton( ) } + isInvalidDraft -> { + ClodyButton( + onClick = { /* no-action */ }, + text = "답장확인", + enabled = false, + modifier = modifier, + disabledContainerColor = ClodyTheme.colors.gray05, + disabledContentColor = ClodyTheme.colors.white, + ) + } + canReply -> { ClodyReplyButton( onClick = onClickReplyDiary, From edd7114adadffd855a928af25cffb2efdbc70a78 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 17:37:51 +0900 Subject: [PATCH 156/299] =?UTF-8?q?[REFACTOR/#280]=20MonthlyCalendarRespon?= =?UTF-8?q?seDto=EC=99=80=20WriteDiaryResponseDto=EC=97=90=20date=20?= =?UTF-8?q?=EB=B0=8F=20isFromDraft=20=ED=95=84=EB=93=9C=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 --- .../clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt | 1 + .../sopt/clody/data/remote/dto/response/WriteDiaryResponseDto.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt index 7a85c9ee..2a4e63ec 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt @@ -14,5 +14,6 @@ data class MonthlyCalendarResponseDto( @SerialName("diaryCount") val diaryCount: Int, @SerialName("replyStatus") val replyStatus: ReplyStatus, @SerialName("isDeleted") val isDeleted: Boolean, + @SerialName("date") val date: String, ) } diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/WriteDiaryResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/WriteDiaryResponseDto.kt index 31b4b1cb..5e0e04bb 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/WriteDiaryResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/WriteDiaryResponseDto.kt @@ -6,4 +6,5 @@ import kotlinx.serialization.Serializable data class WriteDiaryResponseDto( val createdAt: String, val replyType: String, + val isFromDraft: Boolean, ) From 2dac98a89ed6be7a1ab596c2c5a419a7f2f4b8ac Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 17:38:54 +0900 Subject: [PATCH 157/299] =?UTF-8?q?[REFACTOR/#280]=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=9D=BC=EA=B8=B0=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=20=EB=B0=8F=20=EC=B4=88=EA=B8=B0=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=A1=9C=EB=94=A9=20=EB=B0=A9=EC=8B=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 --- .../presentation/ui/home/screen/HomeScreen.kt | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 9fd38af7..1baa0297 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -46,6 +46,7 @@ import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme +import kotlinx.coroutines.delay import java.time.LocalDate @Composable @@ -77,6 +78,7 @@ fun HomeRoute( val deleteDiaryState by homeViewModel.deleteDiaryState.collectAsStateWithLifecycle() val (isError, errorMessage) = homeViewModel.errorState.collectAsStateWithLifecycle().value val showYearMonthPickerState by homeViewModel.showYearMonthPickerState.collectAsStateWithLifecycle() + val hasDraft by homeViewModel.hasDraft.collectAsStateWithLifecycle() LaunchedEffect(Unit) { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) @@ -87,16 +89,14 @@ fun HomeRoute( } } - LaunchedEffect(selectedDiaryDate.year, selectedDiaryDate.month, selectedDate.dayOfMonth) { - homeViewModel.refreshCalendarDataCalendarData(selectedDiaryDate.year, selectedDiaryDate.month) + LaunchedEffect(Unit) { + val year = selectedDiaryDate.year + val month = selectedDiaryDate.month + val day = selectedDate.dayOfMonth - if (selectedDate.dayOfMonth != 0) { - homeViewModel.updateSelectedDate( - LocalDate.of(selectedDiaryDate.year, selectedDiaryDate.month, selectedDate.dayOfMonth), - ) - } else { - homeViewModel.updateSelectedDate(LocalDate.now()) - } + homeViewModel.loadCalendarData(year, month) + delay(500) + homeViewModel.loadDailyDiariesData(year, month, day) } if (isError) { FailureScreen( @@ -122,10 +122,8 @@ fun HomeRoute( onClickDiaryList = navigateToDiaryList, onClickSetting = navigateToSetting, onClickWriteDiary = { year, month, day -> - val hasDraft = replyStatus == ReplyStatus.HAS_DRAFT - val isValidDraftDate = homeViewModel.isValidDraftDate() AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_WRITING_DIARY) - if (hasDraft && isValidDraftDate) { + if (hasDraft && !homeViewModel.isValidDraftDate()) { homeViewModel.setShowContinueDraftDialog(true) } else { navigateToWriteDiary(year, month, day) @@ -146,9 +144,10 @@ fun HomeRoute( selectedYear = selectedDiaryDate.year, selectedMonth = selectedDiaryDate.month, selectedDate = selectedDate, - hasDraft = replyStatus == ReplyStatus.HAS_DRAFT, + hasDraft = hasDraft, canWrite = homeViewModel.canWriteDiary(), canReply = homeViewModel.canReplyDiary(), + isInvalidDraft = replyStatus == ReplyStatus.INVALID_DRAFT, ) if (showFirstDraftPopup) { @@ -303,6 +302,7 @@ fun HomeScreen( hasDraft: Boolean, canWrite: Boolean, canReply: Boolean, + isInvalidDraft: Boolean, ) { if (isError) { FailureScreen( @@ -395,6 +395,7 @@ fun HomeScreen( hasDraft = hasDraft, canWrite = canWrite, canReply = canReply, + isInvalidDraft = isInvalidDraft, year = selectedYear, month = selectedMonth, day = selectedDate.dayOfMonth, From 2e6f4b3f6783718dd1117265046f2e078a771156 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 17:39:11 +0900 Subject: [PATCH 158/299] =?UTF-8?q?[REFACTOR/#280]=20HomeViewModel?= =?UTF-8?q?=EC=97=90=20=EC=B4=88=EA=B8=B0=20=EC=83=81=ED=83=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=ED=95=98=EB=8A=94=20hasDraft=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 --- .../clody/presentation/ui/home/screen/HomeViewModel.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 939ef899..f5706b9b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -93,6 +93,9 @@ class HomeViewModel @Inject constructor( private val _errorState = MutableStateFlow>(false to "") val errorState: StateFlow> = _errorState + private val _hasDraft = MutableStateFlow(false) + val hasDraft: StateFlow get() = _hasDraft + private var isInitialized = false init { @@ -143,9 +146,10 @@ class HomeViewModel @Inject constructor( _dailyDiariesState.value = DailyDiariesState.Loading val result = diaryRepository.getDailyDiariesData(year, month, date) _dailyDiariesState.value = result.fold( - onSuccess = { + onSuccess = { dailyResponse -> + _hasDraft.value = dailyResponse.isDraft setErrorState(false) - DailyDiariesState.Success(it) + DailyDiariesState.Success(dailyResponse) }, onFailure = { exception -> setErrorState(true, exception.message ?: ErrorMessages.UNKNOWN_ERROR) From f5d571730e107b3bb9d15ff8a36a5adb13197fd8 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 17:39:44 +0900 Subject: [PATCH 159/299] =?UTF-8?q?[REFACTOR/#280]=20WriteDiaryViewModel?= =?UTF-8?q?=EC=97=90=20=EB=A7=8C=EB=A3=8C=EB=90=9C=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/writediary/screen/WriteDiaryViewModel.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index a63ee74d..0219fb61 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -20,6 +20,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import java.time.LocalDate import javax.inject.Inject @HiltViewModel @@ -79,9 +80,13 @@ class WriteDiaryViewModel @Inject constructor( val result = diaryRepository.writeDiary(date, contents) _writeDiaryState.value = result.fold( onSuccess = { response -> - when (response.replyType) { - "DELETED" -> WriteDiaryState.NoReply - else -> WriteDiaryState.Success(response.createdAt) + if (isDiaryExpired(year, month, day)) { + WriteDiaryState.NoReply + } else { + when (response.replyType) { + "DELETED" -> WriteDiaryState.NoReply + else -> WriteDiaryState.Success(response.createdAt) + } } }, onFailure = { @@ -97,6 +102,12 @@ class WriteDiaryViewModel @Inject constructor( } } + private fun isDiaryExpired(year: Int, month: Int, day: Int): Boolean { + val today = LocalDate.now() + val diaryDate = LocalDate.of(year, month, day) + return diaryDate.isBefore(today) + } + fun resetFailureDialog() { _showFailureDialog.value = false _failureMessage.value = "" From a78c51fd425f9575bb0a892862896a5828cc22cb Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 17:47:11 +0900 Subject: [PATCH 160/299] =?UTF-8?q?[REFACTOR/#280]=20=ED=95=98=EB=A3=A8=20?= =?UTF-8?q?=EC=A0=84=EC=9D=80=20=EC=9C=A0=ED=9A=A8=ED=95=98=EB=8F=84?= =?UTF-8?q?=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 --- .../presentation/ui/writediary/screen/WriteDiaryViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index 0219fb61..58fcfb80 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -103,9 +103,9 @@ class WriteDiaryViewModel @Inject constructor( } private fun isDiaryExpired(year: Int, month: Int, day: Int): Boolean { - val today = LocalDate.now() val diaryDate = LocalDate.of(year, month, day) - return diaryDate.isBefore(today) + val yesterday = LocalDate.now().minusDays(1) + return diaryDate.isBefore(yesterday) } fun resetFailureDialog() { From a8c45d52f1badb0b991303c8b37fe2533086ccf9 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 19:33:23 +0900 Subject: [PATCH 161/299] [REFACTOR/#280] update cd workflow --- .github/workflows/android_cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index b6f4a12b..d0df028d 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -74,8 +74,8 @@ jobs: echo "baseUrl=$BASE_URL" >> local.properties echo "kakao.api.key=$KAKAO_API_KEY" >> local.properties echo "amplitude.api.key=$AMPLITUDE_API_KEY" >> local.properties - echo "googleAdmob.app.id=$GOOGLE_ADMOB_APP_ID" >> local.properties - echo "googleAdmob.unit.id=$GOOGLE_ADMOB_UNIT_ID" >> local.properties + echo "GOOGLE_ADMOB_APP_ID=$GOOGLE_ADMOB_APP_ID" >> local.properties + echo "GOOGLE_ADMOB_UNIT_ID=$GOOGLE_ADMOB_UNIT_ID" >> local.properties echo "storeFile=keystore/clody_release.jks" >> local.properties echo "storePassword=$STORE_PASSWORD" >> local.properties echo "keyAlias=$KEY_ALIAS" >> local.properties From c93586cb4e14664b6cc17e699753477f9a597240 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 19:49:31 +0900 Subject: [PATCH 162/299] [REFACTOR/#280] update cd workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 추후 flavor이나 build variant로 debug release 분리 할 것 --- .github/workflows/android_cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index d0df028d..bf16562a 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -62,7 +62,7 @@ jobs: # local.properties - name: Generate local.properties env: - BASE_URL: ${{ secrets.BASE_URL }} + TEST_BASE_URL: ${{ secrets.TEST_BASE_URL }} KAKAO_API_KEY: ${{ secrets.KAKAO_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} GOOGLE_ADMOB_APP_ID: ${{ secrets.GOOGLE_ADMOB_APP_ID }} @@ -71,7 +71,7 @@ jobs: KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | - echo "baseUrl=$BASE_URL" >> local.properties + echo "clody.test.url=$TEST_BASE_URL" >> local.properties echo "kakao.api.key=$KAKAO_API_KEY" >> local.properties echo "amplitude.api.key=$AMPLITUDE_API_KEY" >> local.properties echo "GOOGLE_ADMOB_APP_ID=$GOOGLE_ADMOB_APP_ID" >> local.properties From 4135b973c41689443b5c30b1229c42f5f84ae1a8 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 20:03:02 +0900 Subject: [PATCH 163/299] [REFACTOR/#280] release build -> debug build apk --- .github/workflows/android_cd.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index bf16562a..71d8fb10 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -81,9 +81,9 @@ jobs: echo "keyAlias=$KEY_ALIAS" >> local.properties echo "keyPassword=$KEY_PASSWORD" >> local.properties - # Release APK 빌드 - - name: Build Release APK - run: ./gradlew assembleRelease --stacktrace + # Debug APK 빌드 + - name: Build Debug APK + run: ./gradlew assembleDebug --stacktrace # Set up Firebase Service Account Credentials - name: Set up Firebase Service Account Credentials @@ -126,7 +126,7 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS=$HOME/firebase-credentials.json echo "GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS" - firebase appdistribution:distribute app/build/outputs/apk/release/app-release.apk \ + firebase appdistribution:distribute app/build/outputs/apk/debug/app-debug.apk \ --app "$FIREBASE_APP_ID" \ --release-notes "🍀 새로운 테스트 버전이 업로드되었습니다~" \ --groups "clody-tester-group" From 1a28f89fe265e92e93a178662e7e4e9e4c2f9d09 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 20:10:22 +0900 Subject: [PATCH 164/299] [REFACTOR/#280] add base_url --- .github/workflows/android_cd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 71d8fb10..dd7a1a8d 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -62,6 +62,7 @@ jobs: # local.properties - name: Generate local.properties env: + BASE_URL: ${{ secrets.BASE_URL }} TEST_BASE_URL: ${{ secrets.TEST_BASE_URL }} KAKAO_API_KEY: ${{ secrets.KAKAO_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} @@ -71,6 +72,7 @@ jobs: KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | + echo "clody.base.url=$BASE_URL" >> local.properties echo "clody.test.url=$TEST_BASE_URL" >> local.properties echo "kakao.api.key=$KAKAO_API_KEY" >> local.properties echo "amplitude.api.key=$AMPLITUDE_API_KEY" >> local.properties From ce81213a38d9e46a4e5176bec83fb706fe366af0 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 23:54:19 +0900 Subject: [PATCH 165/299] =?UTF-8?q?[DEL/#280]=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/clody/FakeDiaryRepository.kt | 55 ------------ .../com/sopt/clody/WriteDiaryViewModelTest.kt | 86 ------------------- .../repositoryimpl/DiaryRepositoryImplTest.kt | 59 ------------- 3 files changed, 200 deletions(-) delete mode 100644 app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt delete mode 100644 app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt delete mode 100644 app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt diff --git a/app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt b/app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt deleted file mode 100644 index 9355bd74..00000000 --- a/app/src/test/java/com/sopt/clody/FakeDiaryRepository.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.sopt.clody - -import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto -import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto -import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto -import com.sopt.clody.domain.model.CreatedDraftDiaryInfo -import com.sopt.clody.domain.model.DraftDiaryContents -import com.sopt.clody.domain.repository.DiaryRepository - -class FakeDiaryRepository : DiaryRepository { - var draftDiaryResult: Result? = null - var saveDraftResult: Result? = null - private var draftDiaryContents: DraftDiaryContents? = null - - override suspend fun writeDiary(date: String, content: List): Result { - throw NotImplementedError("This method is not implemented in FakeDiaryRepository") - } - - override suspend fun deleteDailyDiary(year: Int, month: Int, day: Int): Result { - throw NotImplementedError("This method is not implemented in FakeDiaryRepository") - } - - override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): Result { - throw NotImplementedError("This method is not implemented in FakeDiaryRepository") - } - - override suspend fun getDiaryTime(year: Int, month: Int, date: Int): Result { - throw NotImplementedError("This method is not implemented in FakeDiaryRepository") - } - - override suspend fun getMonthlyCalendarData(year: Int, month: Int): Result { - throw NotImplementedError("This method is not implemented in FakeDiaryRepository") - } - - override suspend fun getMonthlyDiary(year: Int, month: Int): Result { - throw NotImplementedError("This method is not implemented in FakeDiaryRepository") - } - - override suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result { - throw NotImplementedError("This method is not implemented in FakeDiaryRepository") - } - - override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result { - return draftDiaryContents?.let { Result.success(it) } - ?: Result.failure(IllegalStateException("No draft found")) - } - - override suspend fun saveDraftDiary(contents: List): Result { - draftDiaryContents = DraftDiaryContents(contents) - return Result.success(CreatedDraftDiaryInfo(createdAt = "2024-06-01T00:00:00")) - } -} diff --git a/app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt b/app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt deleted file mode 100644 index 31c1a2bc..00000000 --- a/app/src/test/java/com/sopt/clody/WriteDiaryViewModelTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.sopt.clody - -import com.sopt.clody.domain.model.CreatedDraftDiaryInfo -import com.sopt.clody.domain.model.DraftDiaryContents -import com.sopt.clody.domain.usecase.FetchDraftDiaryUseCase -import com.sopt.clody.domain.usecase.SaveDraftDiaryUseCase -import com.sopt.clody.presentation.ui.writediary.screen.WriteDiaryViewModel -import io.kotest.assertions.nondeterministic.eventually -import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.shouldBe -import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import kotlin.time.Duration.Companion.seconds - -@OptIn(ExperimentalCoroutinesApi::class) -class WriteDiaryViewModelTest : BehaviorSpec( - { - - val testDispatcher = UnconfinedTestDispatcher() - - beforeTest { - Dispatchers.setMain(testDispatcher) - } - - afterTest { - Dispatchers.resetMain() - } - - Given("fetchDraftDiary 호출 시") { - val mockContents = listOf("entry1", "entry2", "entry3") - val fakeRepo = FakeDiaryRepository().apply { - draftDiaryResult = Result.success(DraftDiaryContents(mockContents)) - } - - val viewModel = WriteDiaryViewModel( - diaryRepository = fakeRepo, - fetchDraftDiaryUseCase = FetchDraftDiaryUseCase(fakeRepo), - saveDraftDiaryUseCase = SaveDraftDiaryUseCase(fakeRepo), - networkUtil = mockk(relaxed = true), - ) - - When("fetchDraftDiaryUseCase가 성공하면") { - viewModel.fetchDraftDiary(2025, 6, 1) - - Then("entries와 showWarnings가 초기화된다") { - eventually(duration = 2.seconds) { - viewModel.entries shouldContainExactly mockContents - viewModel.showWarnings shouldContainExactly List(mockContents.size) { false } - } - } - } - } - - Given("saveDraftDiary 호출 시") { - val fakeRepo = FakeDiaryRepository().apply { - saveDraftResult = Result.success(CreatedDraftDiaryInfo("2025-06-01T00:00:00.000Z")) - } - - val viewModel = WriteDiaryViewModel( - diaryRepository = fakeRepo, - fetchDraftDiaryUseCase = FetchDraftDiaryUseCase(fakeRepo), - saveDraftDiaryUseCase = SaveDraftDiaryUseCase(fakeRepo), - networkUtil = mockk(relaxed = true), - ) - - viewModel.updateEntry(0, "entry1") - viewModel.addEntry() - viewModel.updateEntry(1, "entry2") - - viewModel.saveDraftDiary() - - Then("에러 메시지가 설정되지 않는다") { - eventually(duration = 3.seconds) { - viewModel.showFailureDialog.value shouldBe false - viewModel.failureMessage.value shouldBe "" - println("dialog = ${viewModel.showFailureDialog.value}, message = ${viewModel.failureMessage.value}") - } - } - } - }, -) diff --git a/app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt b/app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt deleted file mode 100644 index 6acd516d..00000000 --- a/app/src/test/java/com/sopt/clody/repositoryimpl/DiaryRepositoryImplTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.sopt.clody.repositoryimpl - -import com.sopt.clody.data.repositoryimpl.DiaryRepositoryImpl -import com.sopt.clody.datasource.FakeDiaryRemoteDataSource -import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.matchers.shouldBe - -class DiaryRepositoryImplTest : BehaviorSpec({ - - Given("임시저장 다이어리 조회 기능") { - - val fakeDiaryRemoteDataSource = FakeDiaryRemoteDataSource() - val diaryRepository = DiaryRepositoryImpl(fakeDiaryRemoteDataSource) - - When("유효한 날짜를 전달받으면") { - - Then("해당 날짜의 임시저장 데이터를 정상적으로 반환한다") { - // arrange - val mockData = listOf("오늘도 고생했어", "하루 정리 완료") - fakeDiaryRemoteDataSource.setDraftDiariesResponse(mockData) - - // act - val result = diaryRepository.fetchDraftDiary(2025, 5, 31) - - // assert - println("fetchDraftDiary result: $result") - println("fetchDraftDiary error: ${result.exceptionOrNull()?.message}") - - result.isSuccess shouldBe true - result.getOrNull()?.draftDiaries shouldBe mockData - } - } - } - - Given("임시저장 다이어리 저장 기능") { - - val fakeDiaryRemoteDataSource = FakeDiaryRemoteDataSource() - val diaryRepository = DiaryRepositoryImpl(fakeDiaryRemoteDataSource) - - When("유효한 데이터로 저장 요청을 하면") { - - Then("정상적으로 저장된 시간 정보를 반환한다") { - // arrange - val createdAt = "2025-05-31T20:43:20.696606" - fakeDiaryRemoteDataSource.setSaveDraftDiaryResponse(createdAt) - - // act - val result = diaryRepository.saveDraftDiary(listOf("Test")) - - // assert - println("saveDraftDiary result: $result") - println("saveDraftDiary error: ${result.exceptionOrNull()?.message}") - - result.isSuccess shouldBe true - result.getOrNull()?.createdAt shouldBe createdAt - } - } - } -},) From f507c06128e3b4850bbc67489648be5dbef12ae9 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 23:54:33 +0900 Subject: [PATCH 166/299] =?UTF-8?q?[REFACTOR/#280]=20UX=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=ED=8C=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/calendar/component/DailyDiaryListItem.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt index 963a3af0..bceeb205 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt @@ -92,7 +92,7 @@ fun DailyDiaryListItem( .padding(vertical = 44.dp), ) { Text( - text = "아직 감사 일기가 없어요!", + text = "작성된 감사 일기가 없어요!", style = ClodyTheme.typography.body3Regular, color = ClodyTheme.colors.gray05, textAlign = TextAlign.Center, From 09153e91320ce88ada2bd02cf71a187f9d2079aa Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 16 Jun 2025 23:54:46 +0900 Subject: [PATCH 167/299] =?UTF-8?q?[REFACTOR/#280]=20cd=20workflow=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 --- .github/workflows/android_cd.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index dd7a1a8d..212dc11f 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -62,8 +62,7 @@ jobs: # local.properties - name: Generate local.properties env: - BASE_URL: ${{ secrets.BASE_URL }} - TEST_BASE_URL: ${{ secrets.TEST_BASE_URL }} + CLODY_TEST_URL: ${{ secrets.CLODY_TEST_URL }} KAKAO_API_KEY: ${{ secrets.KAKAO_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} GOOGLE_ADMOB_APP_ID: ${{ secrets.GOOGLE_ADMOB_APP_ID }} @@ -72,8 +71,7 @@ jobs: KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | - echo "clody.base.url=$BASE_URL" >> local.properties - echo "clody.test.url=$TEST_BASE_URL" >> local.properties + echo "clody.test.url=$CLODY_TEST_URL" >> local.properties echo "kakao.api.key=$KAKAO_API_KEY" >> local.properties echo "amplitude.api.key=$AMPLITUDE_API_KEY" >> local.properties echo "GOOGLE_ADMOB_APP_ID=$GOOGLE_ADMOB_APP_ID" >> local.properties From 77397c9b486ee4891b2ed813132f17450a2f8a42 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 17 Jun 2025 00:45:00 +0900 Subject: [PATCH 168/299] =?UTF-8?q?[REFACTOR/#280]=20cd=20workflow=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 --- .github/workflows/android_cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 212dc11f..074df41a 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -62,7 +62,7 @@ jobs: # local.properties - name: Generate local.properties env: - CLODY_TEST_URL: ${{ secrets.CLODY_TEST_URL }} + CLODY_BASE_URL: ${{ secrets.CLODY_TEST_URL }} KAKAO_API_KEY: ${{ secrets.KAKAO_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} GOOGLE_ADMOB_APP_ID: ${{ secrets.GOOGLE_ADMOB_APP_ID }} @@ -71,7 +71,7 @@ jobs: KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | - echo "clody.test.url=$CLODY_TEST_URL" >> local.properties + echo "clody.test.url=$CLODY_BASE_URL" >> local.properties echo "kakao.api.key=$KAKAO_API_KEY" >> local.properties echo "amplitude.api.key=$AMPLITUDE_API_KEY" >> local.properties echo "GOOGLE_ADMOB_APP_ID=$GOOGLE_ADMOB_APP_ID" >> local.properties From 6dc1a330ed260fd15f3203b619e262e1604e95ec Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 17 Jun 2025 00:57:00 +0900 Subject: [PATCH 169/299] =?UTF-8?q?[REFACTOR/#280]=20cd=20workflow=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 --- .github/workflows/android_cd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 074df41a..12d8a49b 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -71,6 +71,7 @@ jobs: KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | + echo "clody.base.url=$CLODY_BASE_URL" >> local.properties echo "clody.test.url=$CLODY_BASE_URL" >> local.properties echo "kakao.api.key=$KAKAO_API_KEY" >> local.properties echo "amplitude.api.key=$AMPLITUDE_API_KEY" >> local.properties From 4fcb163072a40b672c6107ae6d1480fa0fe6cdd4 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 17 Jun 2025 01:41:07 +0900 Subject: [PATCH 170/299] =?UTF-8?q?[ADD/#280]=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/data/remote/dto/response/DailyDiariesResponseDto.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/DailyDiariesResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/DailyDiariesResponseDto.kt index 10544990..006e019e 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/DailyDiariesResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/DailyDiariesResponseDto.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable @Serializable data class DailyDiariesResponseDto( @SerialName("diaries") val diaries: List, + @SerialName("isDeleted") val isDeleted: Boolean, @SerialName("isDraft") val isDraft: Boolean, ) { @Serializable From 3582012119b6c6b3ec2e46956168c139061c690f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 17 Jun 2025 01:41:22 +0900 Subject: [PATCH 171/299] [REFACTOR/#280] Update diary state management --- .../sopt/clody/presentation/ui/home/screen/HomeViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index f5706b9b..b6c6e43a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -148,6 +148,10 @@ class HomeViewModel @Inject constructor( _dailyDiariesState.value = result.fold( onSuccess = { dailyResponse -> _hasDraft.value = dailyResponse.isDraft + _diaryCount.value = dailyResponse.diaries.size + _isDeleted.value = dailyResponse.isDeleted + _replyStatus.value = ReplyStatus.UNREADY + setErrorState(false) DailyDiariesState.Success(dailyResponse) }, From 1d83e390f4f230f42cdc972a6d362de607bc30ea Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 17 Jun 2025 01:41:50 +0900 Subject: [PATCH 172/299] =?UTF-8?q?[REFACTOR/#280]=20coroutine=20delay=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20async=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/home/screen/HomeScreen.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 1baa0297..a22ae328 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -46,7 +46,8 @@ import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme -import kotlinx.coroutines.delay +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import java.time.LocalDate @Composable @@ -94,9 +95,17 @@ fun HomeRoute( val month = selectedDiaryDate.month val day = selectedDate.dayOfMonth - homeViewModel.loadCalendarData(year, month) - delay(500) - homeViewModel.loadDailyDiariesData(year, month, day) + try { + coroutineScope { + val calendarDeferred = async { homeViewModel.loadCalendarData(year, month) } + val dailyDeferred = async { homeViewModel.loadDailyDiariesData(year, month, day) } + + calendarDeferred.await() + dailyDeferred.await() + } + } catch (e: Exception) { + homeViewModel.setErrorState(true, "데이터를 불러오는데 실패했습니다.") + } } if (isError) { FailureScreen( From cb11c8eb1186f5c664c0b362b13d2439b220d3f5 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 17 Jun 2025 01:50:14 +0900 Subject: [PATCH 173/299] =?UTF-8?q?[MOD/#280]=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=83=81=ED=83=9C=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 --- .../com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index b6c6e43a..fd3d3b81 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -150,7 +150,6 @@ class HomeViewModel @Inject constructor( _hasDraft.value = dailyResponse.isDraft _diaryCount.value = dailyResponse.diaries.size _isDeleted.value = dailyResponse.isDeleted - _replyStatus.value = ReplyStatus.UNREADY setErrorState(false) DailyDiariesState.Success(dailyResponse) From 11a25f755690e08a060751f0c59ff3962e2575c0 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:16:44 +0900 Subject: [PATCH 174/299] =?UTF-8?q?[ADD/#283]=20=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A7=81=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 77 +++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b7880a66..ac44e6ff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,11 +1,24 @@ 클로디 + + + 업데이트 필요 + 새로운 버전 %1$s을 사용할 수 있습니다.\n지금 업데이트하시겠습니까? + 업데이트 + 나중에 + + + 필수 업데이트 + 버전 %1$s으로 업데이트가 필요합니다. + 앱 종료 + 행운을 전하는 감사일기 카카오로 로그인 - + + 만나서 반가워요!\n어떻게 불러 드릴까요? 닉네임을 입력해주세요 다음 @@ -51,7 +64,7 @@ 로디가 답장을 거의 다 써가요!\n조금만 기다려주세요 열어보기 확인 - 답장 확인 + 광고 보고 바로 답장 받기 %1$d월 %2$d일 @@ -74,9 +87,13 @@ 나가기 임시저장 - + 저장 %1$d월 %2$d일 + 추가하기 + 보내기 + 일상 속 작은 감사함을 적어보세요 + 2~50자 까지 입력할 수 있어요. 최대 5개까지 작성할 수 있어요. @@ -96,7 +113,52 @@ 작성된 감사일기가 없어요 %1$s일 /%1$s + 답장 확인 + 임시저장된 일기가 있어요. + 작성된 감사 일기가 없어요! + %1$d. %2$s + %1$s요일 + + + 클로버 %1$d개 + %1$d년 %2$d월 + 월 선택 + + + 이어 쓰기 + 답장 확인 + 일기 쓰기 + + + 데이터를 불러오는데 실패했습니다. + 알 수 없는 오류가 발생했습니다. + 일기 삭제 중 오류가 발생했습니다. + + + 기한이 지나면\n로디의 답장을 받을 수 없어요! + 답장 마감 전에 일기를 이어쓸 수 있도록\n알려드리기 위해서는 알림 설정이 필요해요. + [설정 > 애플리케이션 > 클로디 > 알림 > 알림표시] + 알림 받기 + 다음에 하기 + + + 이어쓰기 알림 설정을 완료했어요. + + + 임시저장된 일기를 이어 쓸까요? + 답장 기한이 지나서 답장은 받을 수 없어요. + 이어쓰기 + 아니오 + + + 정말 일기를 삭제할까요? + 아직 답장이 오지 않았거나 삭제하고\n다시 작성한 일기는 답장을 받을 수 없어요. + 삭제할래요 + 아니요 + + + 월 선택 설정 @@ -142,4 +204,13 @@ 확인 알람 시간 설정을 완료했어요. + + 삭제하기 + 다른 날짜 보기 + 완료 + + + 오전 + 오후 + %1$s %2$s시 %3$s분 From 2811b7c3084365a09a45bdb02b65303a12f56e7a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:17:42 +0900 Subject: [PATCH 175/299] =?UTF-8?q?[ADD/#283]=20AM/PM=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20TimePeriod=20enum=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/utils/extension/TimePeriod.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/extension/TimePeriod.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimePeriod.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimePeriod.kt new file mode 100644 index 00000000..e40ce77f --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimePeriod.kt @@ -0,0 +1,15 @@ +package com.sopt.clody.presentation.utils.extension + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.sopt.clody.R + +enum class TimePeriod(@StringRes val labelResId: Int) { + AM(R.string.time_am), + PM(R.string.time_pm), + ; + + @Composable + fun getLabel(): String = stringResource(labelResId) +} From 30a63668bb9aec10c09778d9f109eae50cfc2a3a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:18:49 +0900 Subject: [PATCH 176/299] =?UTF-8?q?[ADD/#283]=20=EC=9A=94=EC=9D=BC=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EB=8B=A4=EA=B5=AD=EC=96=B4(Korean/English?= =?UTF-8?q?)=20=EC=A7=80=EC=9B=90=20=EA=B8=B0=EB=8A=A5=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 --- .../ui/home/calendar/component/WeekHeader.kt | 87 +++++++++++++------ 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt index ba985f67..9751c3fb 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt @@ -17,26 +17,64 @@ import androidx.compose.ui.unit.dp import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.datetime.DayOfWeek +enum class WeekLang { + KOREAN, ENGLISH +} + +fun DayOfWeek.toKoreanShortLabel(): String { + return when (this) { + DayOfWeek.SUNDAY -> "일" + DayOfWeek.MONDAY -> "월" + DayOfWeek.TUESDAY -> "화" + DayOfWeek.WEDNESDAY -> "수" + DayOfWeek.THURSDAY -> "목" + DayOfWeek.FRIDAY -> "금" + DayOfWeek.SATURDAY -> "토" + } +} + +fun DayOfWeek.toEnglishShortLabel(): String { + return when (this) { + DayOfWeek.SUNDAY -> "Sun" + DayOfWeek.MONDAY -> "Mon" + DayOfWeek.TUESDAY -> "Tue" + DayOfWeek.WEDNESDAY -> "Wed" + DayOfWeek.THURSDAY -> "Thu" + DayOfWeek.FRIDAY -> "Fri" + DayOfWeek.SATURDAY -> "Sat" + } +} + +fun DayOfWeek.getLabel(lang: WeekLang): String { + return when (lang) { + WeekLang.KOREAN -> this.toKoreanShortLabel() + WeekLang.ENGLISH -> this.toEnglishShortLabel() + } +} + @Composable -fun WeekHeader(modifier: Modifier = Modifier, itemWidth: Dp) { - val itemWidth = (LocalConfiguration.current.screenWidthDp.dp - 40.dp) / 7 +fun WeekHeader( + modifier: Modifier = Modifier, + itemWidth: Dp = (LocalConfiguration.current.screenWidthDp.dp - 40.dp) / 7, + lang: WeekLang = WeekLang.KOREAN, +) { + val weekLabelArray = listOf( + DayOfWeek.SUNDAY, + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + DayOfWeek.SATURDAY, + ) + + val labels = weekLabelArray.map { it.getLabel(lang) } + Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier.fillMaxWidth(), ) { - val weekLabelArray = listOf( - DayOfWeek.SUNDAY, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY, - ) - - val koreanWeekLabels = weekLabelArray.map { it.toKoreanShortLabel() } - - koreanWeekLabels.forEach { week -> + labels.forEach { week -> Box( modifier = Modifier.width(itemWidth), contentAlignment = Alignment.Center, @@ -52,19 +90,14 @@ fun WeekHeader(modifier: Modifier = Modifier, itemWidth: Dp) { } } -fun DayOfWeek.toKoreanShortLabel(): String { - return when (this) { - DayOfWeek.SUNDAY -> "일" - DayOfWeek.MONDAY -> "월" - DayOfWeek.TUESDAY -> "화" - DayOfWeek.WEDNESDAY -> "수" - DayOfWeek.THURSDAY -> "목" - DayOfWeek.FRIDAY -> "금" - DayOfWeek.SATURDAY -> "토" - } +@Preview(showBackground = true) +@Composable +fun WeekHeaderKoreanPreview() { + WeekHeader(lang = WeekLang.KOREAN) } -@Composable @Preview(showBackground = true) -fun WeekHeaderPreview() { +@Composable +fun WeekHeaderEnglishPreview() { + WeekHeader(lang = WeekLang.ENGLISH) } From f160c3ee1c9fe7178c8ae50ab3687fc22ba26961 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:19:11 +0900 Subject: [PATCH 177/299] =?UTF-8?q?[DEL/#283]=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/component/button/NextButton.kt | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/NextButton.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/NextButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/NextButton.kt deleted file mode 100644 index 70816f4c..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/NextButton.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.sopt.clody.presentation.ui.auth.component.button - -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -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.unit.dp - -@Composable -fun NextButton( - onClick: () -> Unit, - imageResource: Int, - contentDescription: String? = null, -) { - Box( - modifier = Modifier.run { - size(23.dp) - .clip(CircleShape) - .clickable( - onClick = onClick, - ) - }, - contentAlignment = Alignment.Center, - ) { - Image( - painter = painterResource(id = imageResource), - contentDescription = contentDescription, - ) - } -} From 9b8a3b64c998c6732ef5c20d55bd8600f03c6e4a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:20:20 +0900 Subject: [PATCH 178/299] =?UTF-8?q?[REFACTOR/#283]=20AM/PM=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=84=A0=ED=83=9D=EC=9D=84=20TimePeriod=20enum=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20=ED=95=98=EB=93=9C=EC=BD=94=EB=94=A9=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timepicker/BottomSheetTimePicker.kt | 40 +++++++++---------- .../auth/timereminder/TimeReminderScreen.kt | 24 ++++++----- 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt index 6e486ad9..032d65e8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt @@ -28,12 +28,13 @@ import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.timepicker.ClodyPicker import com.sopt.clody.presentation.ui.component.timepicker.rememberPickerState +import com.sopt.clody.presentation.utils.extension.TimePeriod import com.sopt.clody.ui.theme.ClodyTheme @Composable fun BottomSheetTimePicker( onDismissRequest: () -> Unit, - onRemindTimeSelected: (String, String, String) -> Unit, + onRemindTimeSelected: (TimePeriod, String, String) -> Unit, ) { Surface( modifier = Modifier @@ -49,7 +50,6 @@ fun BottomSheetTimePicker( .wrapContentSize() .background(color = ClodyTheme.colors.white) .padding(horizontal = 24.dp), - ) { Box( modifier = Modifier @@ -76,7 +76,9 @@ fun BottomSheetTimePicker( } } - val amPmItems = remember { listOf("오후", "오전") } + val amPmEnumItems = listOf(TimePeriod.AM, TimePeriod.PM) + val amPmLabelItems = amPmEnumItems.map { it.getLabel() } + val hourItems = remember { (1..12).map { it.toString() } } val minuteItems = remember { listOf("00", "10", "20", "30", "40", "50") } @@ -84,10 +86,7 @@ fun BottomSheetTimePicker( val hourPickerState = rememberPickerState() val minutePickerState = rememberPickerState() - Box( - modifier = Modifier - .fillMaxWidth(), - ) { + Box(modifier = Modifier.fillMaxWidth()) { Box( modifier = Modifier .fillMaxWidth() @@ -96,18 +95,16 @@ fun BottomSheetTimePicker( .background(ClodyTheme.colors.gray08, shape = RoundedCornerShape(8.dp)), ) Row( - modifier = Modifier - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { ClodyPicker( state = amPmPickerState, - items = amPmItems, + items = amPmLabelItems, startIndex = 1, visibleItemsCount = 3, infiniteScroll = false, - modifier = Modifier - .weight(1f), + modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), ) ClodyPicker( @@ -116,8 +113,7 @@ fun BottomSheetTimePicker( startIndex = 8, visibleItemsCount = 5, infiniteScroll = true, - modifier = Modifier - .weight(1f), + modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), ) ClodyPicker( @@ -126,22 +122,22 @@ fun BottomSheetTimePicker( startIndex = 3, visibleItemsCount = 5, infiniteScroll = true, - modifier = Modifier - .weight(1f), + modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), ) } } ClodyButton( onClick = { - onRemindTimeSelected( - amPmPickerState.selectedItem, - hourPickerState.selectedItem, - minutePickerState.selectedItem, - ) + val selectedLabel = amPmPickerState.selectedItem + val selectedPeriod = amPmEnumItems.getOrNull(amPmLabelItems.indexOf(selectedLabel)) ?: TimePeriod.PM + val selectedHour = hourPickerState.selectedItem + val selectedMinute = minutePickerState.selectedItem + + onRemindTimeSelected(selectedPeriod, selectedHour, selectedMinute) onDismissRequest() }, - text = "완료", + text = stringResource(R.string.time_reminder_complete_button), enabled = true, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt index 44726859..e9f9f399 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt @@ -44,6 +44,7 @@ import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.extension.TimePeriod import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -101,11 +102,11 @@ fun TimeReminderRoute( TimeReminderScreen( onStartClick = { - viewModel.setFixedTime("21", "30") + viewModel.setFixedTime(TimePeriod.PM, "21", "30") viewModel.sendNotification(context, isNotificationPermissionGranted.value) }, - onTimeSelected = { amPm, hour, minute -> - viewModel.setSelectedTime(amPm, hour, minute) + onTimeSelected = { period, hour, minute -> + viewModel.setSelectedTime(period, hour, minute) }, onCompleteClick = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.ONBOARDING_ALARM) @@ -118,20 +119,20 @@ fun TimeReminderRoute( @Composable fun TimeReminderScreen( onStartClick: () -> Unit, - onTimeSelected: (String, String, String) -> Unit, + onTimeSelected: (TimePeriod, String, String) -> Unit, onCompleteClick: () -> Unit, isLoading: Boolean, ) { var showBottomSheet by remember { mutableStateOf(false) } - var selectedAmPm by remember { mutableStateOf("오후") } + var selectedTimePeriod by remember { mutableStateOf(TimePeriod.PM) } var selectedHour by remember { mutableStateOf("9") } var selectedMinute by remember { mutableStateOf("30") } - val onRemindTimeSelected: (String, String, String) -> Unit = { amPm, hour, minute -> - selectedAmPm = amPm + val onRemindTimeSelected: (TimePeriod, String, String) -> Unit = { period, hour, minute -> + selectedTimePeriod = period selectedHour = hour selectedMinute = minute - onTimeSelected(amPm, hour, minute) + onTimeSelected(period, hour, minute) } Scaffold( @@ -183,7 +184,12 @@ fun TimeReminderScreen( ) Spacer(modifier = Modifier.height(LocalConfiguration.current.screenHeightDp.dp * 0.05f)) PickerBox( - time = "$selectedAmPm ${selectedHour}시 ${selectedMinute}분", + time = stringResource( + id = R.string.time_reminder_time_format, + selectedTimePeriod.getLabel(), + selectedHour, + selectedMinute, + ), modifier = Modifier.fillMaxWidth(), onClick = { showBottomSheet = true }, ) From a751473817a8c47ca1f20c46f3a0dd7bb79808d9 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:21:15 +0900 Subject: [PATCH 179/299] =?UTF-8?q?[REFACTOR/#283]=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=8B=9C=20AM/PM=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=20=EB=8C=80=EC=8B=A0=20TimePeriod=20enum=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=ED=83=80=EC=9E=85=20=EC=95=88?= =?UTF-8?q?=EC=A0=84=EC=84=B1=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timereminder/TimeReminderViewModel.kt | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt index c8e45835..0d07fbec 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.NotificationRepository +import com.sopt.clody.presentation.utils.extension.TimePeriod import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR @@ -16,6 +17,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import java.util.Locale import javax.inject.Inject @HiltViewModel @@ -77,22 +79,19 @@ class TimeReminderViewModel @Inject constructor( return sharedPreferences.getString("fcm_token", null) } - fun setSelectedTime(amPm: String, hour: String, minute: String) { - selectedTime = formatTime(amPm, hour, minute) + fun setSelectedTime(period: TimePeriod, hour: String, minute: String) { + selectedTime = formatTime(period, hour, minute) } - fun setFixedTime(hour: String, minute: String) { - selectedTime = String.format("%02d:%02d", hour.toInt(), minute.toInt()) + fun setFixedTime(period: TimePeriod, hour: String, minute: String) { + selectedTime = formatTime(period, hour, minute) } - private fun formatTime(amPm: String, hour: String, minute: String): String { - val hourInt = if (amPm == "오후" && hour.toInt() != 12) { - hour.toInt() + 12 - } else if (amPm == "오전" && hour.toInt() == 12) { - 0 - } else { - hour.toInt() + private fun formatTime(period: TimePeriod, hour: String, minute: String): String { + val hourInt = when (period) { + TimePeriod.PM -> if (hour.toInt() != 12) hour.toInt() + 12 else 12 + TimePeriod.AM -> if (hour.toInt() == 12) 0 else hour.toInt() } - return String.format("%02d:%02d", hourInt, minute.toInt()) + return String.format(Locale.US, "%02d:%02d", hourInt, minute.toInt()) } } From 5f0ae0fcb10b3215d6bcd83eaf7cfa873f4ab4ee Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:24:41 +0900 Subject: [PATCH 180/299] =?UTF-8?q?[REFACTOR/#283]=20=ED=95=98=EB=93=9C?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/signup/page/NicknamePage.kt | 7 ++-- .../calendar/component/DailyDiaryListItem.kt | 10 ++++-- .../ui/home/component/CloverCount.kt | 4 ++- .../ui/home/component/DiaryStateButton.kt | 12 ++++--- .../ui/home/component/YearAndMonthTitle.kt | 6 ++-- .../presentation/ui/home/screen/HomeScreen.kt | 33 ++++++++--------- .../component/QuickReplyAdButton.kt | 3 +- .../presentation/ui/splash/SplashScreen.kt | 35 ++++++++++++++----- .../DeleteWriteDiaryBottomSheet.kt | 3 +- .../component/button/AddDiaryEntryFAB.kt | 3 +- .../writediary/component/button/SendButton.kt | 4 ++- .../textfield/WriteDiaryTextField.kt | 5 +-- 12 files changed, 80 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt index 65e3ebc7..a716c61f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle @@ -85,7 +86,7 @@ fun NickNamePage( focusManager.clearFocus() onCompleteClick() }, - text = "다음", + text = stringResource(R.string.nickname_next), enabled = nickname.isNotEmpty() && isValidNickname, ) }, @@ -98,12 +99,12 @@ fun NickNamePage( .padding(horizontal = 24.dp), ) { Spacer(modifier = Modifier.heightForScreenPercentage(0.056f)) - Text("닉네임 입력", style = ClodyTheme.typography.head1) + Text(stringResource(R.string.nickname_title), style = ClodyTheme.typography.head1) Spacer(modifier = Modifier.heightForScreenPercentage(0.06f)) NickNameTextField( value = nickname, onValueChange = onNicknameChange, - hint = "닉네임을 입력하세요", + hint = stringResource(R.string.nickname_input_hint), isFocused = isFocused, isValid = isValidNickname, onFocusChanged = onFocusChanged, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt index bceeb205..21192e99 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt @@ -18,6 +18,7 @@ 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.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sopt.clody.R @@ -52,7 +53,10 @@ fun DailyDiaryListItem( modifier = Modifier.padding(vertical = 3.dp), ) Text( - text = "${dayOfWeek.toKoreanShortLabel()}요일", + text = stringResource( + id = R.string.daily_diary_day_of_week_format, + dayOfWeek.toKoreanShortLabel(), + ), style = ClodyTheme.typography.body2Medium, color = ClodyTheme.colors.gray02, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), @@ -76,7 +80,7 @@ fun DailyDiaryListItem( .padding(vertical = 44.dp), ) { Text( - text = "임시저장된 일기가 있어요.", + text = stringResource(R.string.daily_diary_draft_message), style = ClodyTheme.typography.body3Regular, color = ClodyTheme.colors.gray05, textAlign = TextAlign.Center, @@ -92,7 +96,7 @@ fun DailyDiaryListItem( .padding(vertical = 44.dp), ) { Text( - text = "작성된 감사 일기가 없어요!", + text = stringResource(R.string.daily_diary_empty_message), style = ClodyTheme.typography.body3Regular, color = ClodyTheme.colors.gray05, textAlign = TextAlign.Center, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt index d85e720c..ff1029ba 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt @@ -10,13 +10,15 @@ 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.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.sopt.clody.R import com.sopt.clody.ui.theme.ClodyTheme @Composable fun CloverCount(cloverCount: Int) { - val text = "클로버 ${cloverCount}개" + val text = stringResource(R.string.home_clover_count_format, cloverCount) Box( modifier = Modifier diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt index 851f5eae..5b921f05 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt @@ -4,7 +4,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.button.ClodyReplyButton import com.sopt.clody.ui.theme.ClodyTheme @@ -29,7 +31,7 @@ fun DiaryStateButton( hasDraft -> { ClodyButton( onClick = { onClickWriteDiary(year, month, day) }, - text = "이어쓰기", + text = stringResource(R.string.button_continue_draft), enabled = true, modifier = modifier, ) @@ -38,7 +40,7 @@ fun DiaryStateButton( isInvalidDraft -> { ClodyButton( onClick = { /* no-action */ }, - text = "답장확인", + text = stringResource(R.string.button_check_reply), enabled = false, modifier = modifier, disabledContainerColor = ClodyTheme.colors.gray05, @@ -49,7 +51,7 @@ fun DiaryStateButton( canReply -> { ClodyReplyButton( onClick = onClickReplyDiary, - text = "답장확인", + text = stringResource(R.string.button_check_reply), enabled = true, modifier = modifier, ) @@ -58,7 +60,7 @@ fun DiaryStateButton( canWrite -> { ClodyButton( onClick = { onClickWriteDiary(year, month, day) }, - text = "일기쓰기", + text = stringResource(R.string.button_write_diary), enabled = true, modifier = modifier, ) @@ -67,7 +69,7 @@ fun DiaryStateButton( else -> { ClodyButton( onClick = { onClickWriteDiary(year, month, day) }, - text = "일기쓰기", + text = stringResource(R.string.button_write_diary), enabled = false, modifier = modifier, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt index 32d9e7bf..b9677b37 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.remember 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.unit.dp import com.sopt.clody.R import com.sopt.clody.ui.theme.ClodyTheme @@ -22,7 +23,7 @@ fun YearAndMonthTitle( selectedYear: Int, selectedMonth: Int, ) { - val text = "${selectedYear}년 ${selectedMonth}월" + val text = stringResource(R.string.home_year_and_month_format, selectedYear, selectedMonth) Column { Row( @@ -41,8 +42,7 @@ fun YearAndMonthTitle( Image( painter = painterResource(id = R.drawable.ic_home_under_arrow), contentDescription = "choose month", - modifier = Modifier - .padding(horizontal = 6.dp, vertical = 6.dp), + modifier = Modifier.padding(horizontal = 6.dp, vertical = 6.dp), ) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index a22ae328..2ef9cf90 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue 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.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -171,28 +172,28 @@ fun HomeRoute( .padding(horizontal = 16.dp), ) { Text( - text = "기한이 지나면\n로디의 답장을 받을 수 없어요!", + text = stringResource(R.string.home_draft_popup_title), color = ClodyTheme.colors.gray01, textAlign = TextAlign.Center, style = ClodyTheme.typography.head3, ) Spacer(modifier = Modifier.height(10.dp)) Text( - text = "답장 마감 전에 일기를 이어쓸 수 있도록\n알려드리기 위해서는 알림 설정이 필요해요.", + text = stringResource(R.string.home_draft_popup_description), color = ClodyTheme.colors.gray04, textAlign = TextAlign.Center, style = ClodyTheme.typography.body3Regular, ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "[설정 > 애플리케이션 > 클로디 > 알림 > 알림표시]", + text = stringResource(R.string.home_draft_popup_guide), color = ClodyTheme.colors.gray04, textAlign = TextAlign.Center, style = ClodyTheme.typography.body3Regular, ) Spacer(modifier = Modifier.height(28.dp)) ClodyButton( - text = "알림 받기", + text = stringResource(R.string.home_draft_popup_accept), onClick = { homeViewModel.enableDraftAlarm(context) homeViewModel.updateFirstDraftUse(false) @@ -201,7 +202,7 @@ fun HomeRoute( modifier = Modifier.fillMaxWidth(), ) Text( - text = "다음에 하기", + text = stringResource(R.string.home_draft_popup_dismiss), modifier = Modifier .clickable(onClick = { homeViewModel.updateFirstDraftUse(false) }) .padding(12.dp), @@ -220,7 +221,7 @@ fun HomeRoute( contentAlignment = Alignment.BottomCenter, content = { ClodyToastMessage( - message = "이어쓰기 알림 설정을 완료했어요.", + message = stringResource(R.string.home_draft_alarm_enabled_toast), iconResId = R.drawable.ic_toast_check_on_18, backgroundColor = ClodyTheme.colors.gray04, contentColor = ClodyTheme.colors.white, @@ -236,10 +237,10 @@ fun HomeRoute( if (showContinueDraftDialog) { ClodyDialog( - titleMassage = "임시저장된 일기를 이어 쓸까요?", - descriptionMassage = "답장 기한이 지나서 답장은 받을 수 없어요.", - confirmOption = "이어쓰기", - dismissOption = "아니오", + titleMassage = stringResource(R.string.home_continue_draft_title), + descriptionMassage = stringResource(R.string.home_continue_draft_description), + confirmOption = stringResource(R.string.home_continue_draft_confirm), + dismissOption = stringResource(R.string.home_continue_draft_dismiss), confirmAction = { homeViewModel.setShowContinueDraftDialog(false) val date = homeViewModel.selectedDate.value @@ -265,10 +266,10 @@ fun HomeRoute( if (showDiaryDeleteDialog) { ClodyDialog( - titleMassage = "정말 일기를 삭제할까요?", - descriptionMassage = "아직 답장이 오지 않았거나 삭제하고\n다시 작성한 일기는 답장을 받을 수 없어요.", - confirmOption = "삭제할래요", - dismissOption = "아니요", + titleMassage = stringResource(R.string.home_delete_diary_title), + descriptionMassage = stringResource(R.string.home_delete_diary_description), + confirmOption = stringResource(R.string.home_delete_diary_confirm), + dismissOption = stringResource(R.string.home_delete_diary_dismiss), confirmAction = { homeViewModel.deleteDailyDiary( selectedDiaryDate.year, @@ -374,7 +375,7 @@ fun HomeScreen( } is CalendarState.Error -> { - homeViewModel.setErrorState(true, calendarState.message ?: "알 수 없는 오류가 발생했습니다.") + homeViewModel.setErrorState(true, calendarState.message ?: stringResource(R.string.home_error_unknown)) } } @@ -389,7 +390,7 @@ fun HomeScreen( } is DeleteDiaryState.Failure -> { - homeViewModel.setErrorState(true, "일기 삭제 중 오류가 발생했습니다.") + homeViewModel.setErrorState(true, stringResource(R.string.home_error_delete_diary)) } } }, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/component/QuickReplyAdButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/component/QuickReplyAdButton.kt index 3507cd0e..49ddc451 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/component/QuickReplyAdButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/component/QuickReplyAdButton.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip 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.sopt.clody.R @@ -40,7 +41,7 @@ fun QuickReplyAdButton( tint = Color.Unspecified, ) Text( - text = "광고 보고 바로 답장 받기", + text = stringResource(R.string.loading_button_watch_ad_and_get_reply), style = ClodyTheme.typography.body4Medium, color = ClodyTheme.colors.blue, modifier = Modifier.padding(start = 5.dp), diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt index 3e3b5ebd..a14ff51f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.unit.dp import androidx.lifecycle.compose.LocalLifecycleOwner @@ -120,15 +121,23 @@ fun SoftUpdateDialog( ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text("업데이트 필요") }, + title = { Text(stringResource(R.string.soft_update_title)) }, text = { Text( - text = "새로운 버전 ${latestVersion}을 사용할 수 있습니다.\n지금 업데이트하시겠습니까?", + text = stringResource(R.string.soft_update_message, latestVersion), textAlign = TextAlign.Center, ) }, - confirmButton = { TextButton(onClick = onConfirm) { Text("업데이트") } }, - dismissButton = { TextButton(onClick = onDismiss) { Text("나중에") } }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.soft_update_confirm)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.soft_update_dismiss)) + } + }, ) } @@ -140,10 +149,20 @@ fun HardUpdateDialog( ) { AlertDialog( onDismissRequest = {}, - title = { Text("필수 업데이트") }, - text = { Text("버전 ${latestVersion}으로 업데이트가 필요합니다.") }, - confirmButton = { TextButton(onClick = onConfirm) { Text("업데이트") } }, - dismissButton = { TextButton(onClick = onExit) { Text("앱 종료") } }, + title = { Text(stringResource(R.string.hard_update_title)) }, + text = { + Text(stringResource(R.string.hard_update_message, latestVersion)) + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.soft_update_confirm)) + } + }, + dismissButton = { + TextButton(onClick = onExit) { + Text(stringResource(R.string.hard_update_exit)) + } + }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt index 0a723e1d..890be429 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.remember 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.sopt.clody.R @@ -60,7 +61,7 @@ fun DeleteWriteDiaryBottomSheet( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "삭제하기", + text = stringResource(R.string.bottom_sheet_delete_button), style = ClodyTheme.typography.body4SemiBold, color = ClodyTheme.colors.gray01, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt index d976aac3..9f5b16ec 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt @@ -23,6 +23,7 @@ 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.res.stringResource import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.presentation.utils.extension.clickableWithoutRipple @@ -85,7 +86,7 @@ fun BoxScope.AddDiaryEntryFAB( ) Spacer(modifier = Modifier.width(10.dp)) Text( - text = "추가하기", + text = stringResource(R.string.write_diary_add_entry_fab), color = contentColor, style = ClodyTheme.typography.body2SemiBold, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt index 7d69561c..059e94e6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt @@ -11,7 +11,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.sopt.clody.R import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -33,7 +35,7 @@ fun SendButton( contentAlignment = Alignment.Center, ) { Text( - text = "보내기", + text = stringResource(R.string.write_diary_text_button), color = if (isPressed) ClodyTheme.colors.gray07 else ClodyTheme.colors.gray01, style = ClodyTheme.typography.body2SemiBold, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt index e2593bde..1c495420 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString @@ -130,7 +131,7 @@ fun WriteDiaryTextField( decorationBox = { innerTextField -> if (text.isEmpty()) { Text( - text = "일상 속 작은 감사함을 적어보세요", + text = stringResource(R.string.write_diary_text_field_hint), style = ClodyTheme.typography.body3Medium, color = ClodyTheme.colors.gray06, ) @@ -165,7 +166,7 @@ fun WriteDiaryTextField( ) { if ((showWarning && !isTextValid) || isTextTooLong) { Text( - text = "2~50자 까지 입력할 수 있어요.", + text = stringResource(R.string.write_diary_text_field_warning), color = ClodyTheme.colors.red, style = ClodyTheme.typography.detail1Medium, modifier = Modifier.weight(1f), From 723333e6199bde489d480ece3313dfdd0249577a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:24:57 +0900 Subject: [PATCH 181/299] =?UTF-8?q?[MOD/#283]=20=EC=95=88=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20string=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac44e6ff..55136bc7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -123,7 +123,6 @@ 클로버 %1$d개 %1$d년 %2$d월 - 월 선택 이어 쓰기 From b2b5aec652a52f35f8ba861c62c524eace61acec Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:25:13 +0900 Subject: [PATCH 182/299] =?UTF-8?q?[REFACTOR/#283]=20=ED=95=98=EB=93=9C?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/component/bottomsheet/DiaryDeleteSheet.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/bottomsheet/DiaryDeleteSheet.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/bottomsheet/DiaryDeleteSheet.kt index ba5f2b4e..feecc8e6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/bottomsheet/DiaryDeleteSheet.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/bottomsheet/DiaryDeleteSheet.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.remember 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.unit.dp import com.sopt.clody.R import com.sopt.clody.ui.theme.ClodyTheme @@ -71,7 +72,7 @@ fun DiaryDeleteBottomSheetItem( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "삭제하기", + text = stringResource(R.string.bottom_sheet_delete_button), style = ClodyTheme.typography.body4SemiBold, color = ClodyTheme.colors.gray01, ) From 9ea13383f17f658441d2e5e79974c7071817a8bf Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:37:28 +0900 Subject: [PATCH 183/299] =?UTF-8?q?[ADD/#283]=20=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A7=81=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 55136bc7..f5416386 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -212,4 +212,7 @@ 오전 오후 %1$s %2$s시 %3$s분 + + %1$d년 + %1$d월 From d3a549c1f9c25062ad618d664c5acc09ab6f44eb Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:37:50 +0900 Subject: [PATCH 184/299] =?UTF-8?q?[ADD/#283]=20=EC=97=B0=EB=8F=84=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9B=94=20=EB=A0=88=EC=9D=B4=EB=B8=94=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=9C=A0=ED=8B=B8=EB=A6=AC=ED=8B=B0=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 --- .../utils/extension/YearMonthLabelUtil.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/extension/YearMonthLabelUtil.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/YearMonthLabelUtil.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/YearMonthLabelUtil.kt new file mode 100644 index 00000000..cbddfeb0 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/YearMonthLabelUtil.kt @@ -0,0 +1,16 @@ +package com.sopt.clody.presentation.utils.extension + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.sopt.clody.R + +object YearMonthLabelUtil { + const val MIN_YEAR = 2000 + const val MAX_YEAR = 2030 +} + +@Composable +fun Int.toLocalizedYearLabel(): String = stringResource(R.string.year_format, this) + +@Composable +fun Int.toLocalizedMonthLabel(): String = stringResource(R.string.month_format, this) From 05a5d8be02f1849fe6adc801ace9eecc0577d5dc Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 21 Jun 2025 17:38:31 +0900 Subject: [PATCH 185/299] =?UTF-8?q?[REFACTOR/#283]=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EB=B0=8F=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A6=AC=ED=8B=B0=20=ED=95=A8=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EC=97=B0=EB=8F=84/=EC=9B=94=20=EB=A0=88=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/timepicker/YearMonthPicker.kt | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt index 9cd81c01..f04f68eb 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt @@ -23,9 +23,13 @@ import androidx.compose.runtime.remember 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.unit.dp import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.button.ClodyButton +import com.sopt.clody.presentation.utils.extension.YearMonthLabelUtil +import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel +import com.sopt.clody.presentation.utils.extension.toLocalizedYearLabel import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -35,6 +39,15 @@ fun YearMonthPicker( selectedMonth: Int, onYearMonthSelected: (Int, Int) -> Unit, ) { + val yearItems = remember { (YearMonthLabelUtil.MIN_YEAR..YearMonthLabelUtil.MAX_YEAR).toList() } + val monthItems = remember { (1..12).toList() } + + val yearPickerState = rememberPickerState() + val monthPickerState = rememberPickerState() + + val startYearIndex = (yearItems.indexOf(selectedYear) - 2).coerceAtLeast(0) + val startMonthIndex = (monthItems.indexOf(selectedMonth) - 2).coerceAtLeast(0) + Surface( modifier = Modifier .fillMaxWidth() @@ -49,7 +62,6 @@ fun YearMonthPicker( .wrapContentSize() .background(color = ClodyTheme.colors.white) .padding(horizontal = 24.dp), - ) { Box( modifier = Modifier @@ -57,7 +69,7 @@ fun YearMonthPicker( .padding(top = 16.dp, bottom = 30.dp), ) { Text( - text = "다른 날짜 보기", + text = stringResource(R.string.year_month_picker_title), style = ClodyTheme.typography.body2SemiBold, color = ClodyTheme.colors.gray01, modifier = Modifier.align(Alignment.Center), @@ -76,19 +88,10 @@ fun YearMonthPicker( } } - val yearItems = remember { (2000..2030).map { "${it}년" } } - val monthItems = remember { (1..12).map { "${it}월" } } - - val yearPickerState = rememberPickerState() - val monthPickerState = rememberPickerState() + val yearLabelItems = yearItems.map { it.toLocalizedYearLabel() } + val monthLabelItems = monthItems.map { it.toLocalizedMonthLabel() } - val startYearIndex = (yearItems.indexOf("${selectedYear}년") - 2).coerceAtLeast(0) - val startMonthIndex = (monthItems.indexOf("${selectedMonth}월") - 2).coerceAtLeast(0) - - Box( - modifier = Modifier - .fillMaxWidth(), - ) { + Box(modifier = Modifier.fillMaxWidth()) { Box( modifier = Modifier .fillMaxWidth() @@ -97,30 +100,27 @@ fun YearMonthPicker( .background(ClodyTheme.colors.gray08, shape = RoundedCornerShape(8.dp)), ) Row( - modifier = Modifier - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Spacer(modifier = Modifier.weight(1f)) YearMonthPickerItem( state = yearPickerState, - items = yearItems, + items = yearLabelItems, startIndex = startYearIndex, visibleItemsCount = 5, infiniteScroll = false, - modifier = Modifier - .weight(1f), + modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), ) Spacer(modifier = Modifier.width(20.dp)) YearMonthPickerItem( state = monthPickerState, - items = monthItems, + items = monthLabelItems, startIndex = startMonthIndex, visibleItemsCount = 5, infiniteScroll = false, - modifier = Modifier - .weight(1f), + modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), ) Spacer(modifier = Modifier.weight(1f)) @@ -128,12 +128,12 @@ fun YearMonthPicker( } ClodyButton( onClick = { - val selectedYear = yearPickerState.selectedItem.split("년")[0].toInt() - val selectedMonth = monthPickerState.selectedItem.split("월")[0].toInt() - onYearMonthSelected(selectedYear, selectedMonth) + val year = yearItems[yearLabelItems.indexOf(yearPickerState.selectedItem)] + val month = monthItems[monthLabelItems.indexOf(monthPickerState.selectedItem)] + onYearMonthSelected(year, month) onDismissRequest() }, - text = "완료", + text = stringResource(R.string.year_month_picker_confirm), enabled = true, modifier = Modifier .fillMaxWidth() From cd70304db7f528029e5849f3c60b1cdfa17e015b Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 22 Jun 2025 01:33:48 +0900 Subject: [PATCH 186/299] =?UTF-8?q?[REFACTOR/#285]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B3=B4=EC=95=88=20=EC=A0=90=EA=B2=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 디바이스 루팅 여부 확인 기능 구현 (test-keys, 루팅 바이너리, su 명령어 탐지 방식) - Chrome 브라우저 설치 여부 확인 기능 추가 --- .../login/DefaultLoginSecurityChecker.kt | 63 +++++++++++++++++++ .../security/login/LoginSecurityChecker.kt | 8 +++ .../core/security/login/SecurityModule.kt | 16 +++++ 3 files changed, 87 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt create mode 100644 app/src/main/java/com/sopt/clody/core/security/login/LoginSecurityChecker.kt create mode 100644 app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt diff --git a/app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt b/app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt new file mode 100644 index 00000000..b133960c --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt @@ -0,0 +1,63 @@ +package com.sopt.clody.core.security.login + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +/** + * 기본 보안 점검 구현체. + * + * 디바이스 루팅 여부 및 Chrome 브라우저 설치 여부를 확인하는 기능 구현. + * + */ + +@Singleton +class DefaultLoginSecurityChecker @Inject constructor() : LoginSecurityChecker { + + /** + * 디바이스가 루팅되었는지 여부를 검사. + * Step + * - `Build.TAGS`에 `test-keys`가 포함되어 있는지 확인 + * - 루팅에 사용되는 바이너리 또는 앱의 존재 여부 검사 + * - `which su` 명령어 실행 결과를 통해 `su` 명령어의 존재 여부 확인 + * + * @return 디바이스가 루팅되었다면 `true`, 그렇지 않다면 `false` + */ + override fun isDeviceRooted(): Boolean { + val buildTags = Build.TAGS + if (buildTags != null && buildTags.contains("test-keys")) return true + + val paths = arrayOf( + "/system/app/Superuser.apk", + "/sbin/su", "/system/bin/su", "/system/xbin/su", + "/data/local/xbin/su", "/data/local/bin/su", + "/system/sd/xbin/su", "/system/bin/failsafe/su", + "/data/local/su", + ) + if (paths.any { File(it).exists() }) return true + + return try { + Runtime.getRuntime().exec(arrayOf("/system/xbin/which", "su")) + .inputStream.bufferedReader().readLine() != null + } catch (e: Exception) { + false + } + } + + /** + * 디바이스에 Chrome 브라우저가 설치되어 있는지 여부 + * + * `com.android.chrome` 패키지의 존재 여부로 Chrome 설치 여부를 판단. + * + * @return Chrome이 설치되어 있다면 `true`, 아니라면 `false` + */ + override fun isChromeInstalled(context: Context): Boolean = try { + context.packageManager.getPackageInfo("com.android.chrome", 0) + true + } catch (e: PackageManager.NameNotFoundException) { + false + } +} diff --git a/app/src/main/java/com/sopt/clody/core/security/login/LoginSecurityChecker.kt b/app/src/main/java/com/sopt/clody/core/security/login/LoginSecurityChecker.kt new file mode 100644 index 00000000..4e0b06e3 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/security/login/LoginSecurityChecker.kt @@ -0,0 +1,8 @@ +package com.sopt.clody.core.security.login + +import android.content.Context + +interface LoginSecurityChecker { + fun isDeviceRooted(): Boolean + fun isChromeInstalled(context: Context): Boolean +} diff --git a/app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt b/app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt new file mode 100644 index 00000000..89bdd485 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt @@ -0,0 +1,16 @@ +package com.sopt.clody.core.security.login + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class SecurityModule { + + @Binds + abstract fun bindLoginSecurityChecker( + impl: DefaultLoginSecurityChecker, + ): LoginSecurityChecker +} From e4bd9f4768e5869db8572a6e890bb7365902349c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 22 Jun 2025 01:36:13 +0900 Subject: [PATCH 187/299] =?UTF-8?q?[REFACTOR/#285]=20Chrome=20=EB=AF=B8?= =?UTF-8?q?=EC=84=A4=EC=B9=98=20=EB=B0=8F=20=EB=A3=A8=ED=8C=85=20=EA=B8=B0?= =?UTF-8?q?=EA=B8=B0=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B0=A8=EB=8B=A8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20-=20KakaoLoginSdk?= =?UTF-8?q?=EC=97=90=20=EB=B3=B4=EC=95=88=20=EC=A0=90=EA=B2=80=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80=20=20=20-=20Chrome=20=EB=B8=8C?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=EC=A0=80=20=EB=AF=B8=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=EC=8B=9C=20=EC=98=88=EC=99=B8=20=EB=B0=9C=EC=83=9D=20=20=20-?= =?UTF-8?q?=20=EB=A3=A8=ED=8C=85=EB=90=9C=20=EA=B8=B0=EA=B8=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=EB=8F=84=20?= =?UTF-8?q?=EC=8B=9C=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/clody/core/login/KakaoLoginSdk.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt b/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt index 216cf86c..d24a3e95 100644 --- a/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt +++ b/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt @@ -6,6 +6,8 @@ import com.kakao.sdk.common.model.AuthError import com.kakao.sdk.common.model.ClientError import com.kakao.sdk.common.model.ClientErrorCause import com.kakao.sdk.user.UserApiClient +import com.sopt.clody.R +import com.sopt.clody.core.security.login.LoginSecurityChecker import kotlinx.coroutines.suspendCancellableCoroutine import javax.inject.Inject import javax.inject.Singleton @@ -13,8 +15,18 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @Singleton -class KakaoLoginSdk @Inject constructor() : LoginSdk { +class KakaoLoginSdk @Inject constructor( + private val securityChecker: LoginSecurityChecker, +) : LoginSdk { override suspend fun login(context: Context): Result = runCatching { + if (!securityChecker.isChromeInstalled(context)) { + throw LoginException.AuthException(context.getString(R.string.error_login_requires_chrome)) + } + + if (securityChecker.isDeviceRooted()) { + throw LoginException.AuthException(context.getString(R.string.error_login_rooted_device)) + } + suspendCancellableCoroutine { continuation -> val callback: (OAuthToken?, Throwable?) -> Unit = callback@{ token, throwable -> if (!continuation.isActive) { From c7ba6910932843931a2a00c25541348f694cb2a7 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 22 Jun 2025 01:37:12 +0900 Subject: [PATCH 188/299] =?UTF-8?q?[FEAT/#285]=20WebView=EC=97=90=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=ED=95=84=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EB=B0=8F=20SSL=20=EC=98=A4=EB=A5=98=20=EC=B0=A8=EB=8B=A8=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 - 허용된 도메인 외 요청은 외부 브라우저로 열도록 처리 - SSL 인증서 오류 발생 시 로딩 차단으로 보안 강화 --- .../security/weview/SecureWebViewClient.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/core/security/weview/SecureWebViewClient.kt diff --git a/app/src/main/java/com/sopt/clody/core/security/weview/SecureWebViewClient.kt b/app/src/main/java/com/sopt/clody/core/security/weview/SecureWebViewClient.kt new file mode 100644 index 00000000..44bd523b --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/security/weview/SecureWebViewClient.kt @@ -0,0 +1,33 @@ +package com.sopt.clody.core.security.weview + +import android.content.Context +import android.content.Intent +import android.net.http.SslError +import android.webkit.SslErrorHandler +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient + +class SecureWebViewClient( + private val context: Context, + private val allowedDomains: List = listOf("notion.so", "forms.gle"), +) : WebViewClient() { + + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + val host = request.url.host ?: return true + val isSafeDomain = allowedDomains.any { host.contains(it) } + + return if (isSafeDomain) { + false + } else { + Intent(Intent.ACTION_VIEW, request.url).let { + context.startActivity(it) + } + true + } + } + + override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { + handler.cancel() + } +} From 5821d0347c7057e6b75fd92940955dc8d27fd176 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 22 Jun 2025 01:38:36 +0900 Subject: [PATCH 189/299] =?UTF-8?q?[ADD/#285]=20WebView=20=ED=97=88?= =?UTF-8?q?=EC=9A=A9=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5ea77adb..db2514de 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,10 +31,13 @@ android { val amplitudeApiKey: String = properties.getProperty("amplitude.api.key") val googleAdmobAppId: String = properties.getProperty("GOOGLE_ADMOB_APP_ID", "") val googleAdmobUnitId: String = properties.getProperty("GOOGLE_ADMOB_UNIT_ID", "") + val allowedDomains: String = properties.getProperty("allowed.webview.domains", "notion.so,google.com") + buildConfigField("String", "GOOGLE_ADMOB_APP_ID", "\"$googleAdmobAppId\"") buildConfigField("String", "GOOGLE_ADMOB_UNIT_ID", "\"$googleAdmobUnitId\"") buildConfigField("String", "KAKAO_API_KEY", "\"$kakaoApiKey\"") buildConfigField("String", "AMPLITUDE_API_KEY", "\"$amplitudeApiKey\"") + buildConfigField("String", "ALLOWED_WEBVIEW_DOMAINS", "\"$allowedDomains\"") manifestPlaceholders["kakaoRedirectUri"] = "kakao$kakaoApiKey" manifestPlaceholders["GOOGLE_ADMOB_APP_ID"] = googleAdmobAppId testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From 79fa27c9738d6ab41d09e7d07aea23e98ca39525 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 22 Jun 2025 01:40:05 +0900 Subject: [PATCH 190/299] =?UTF-8?q?[ADD/#285]=20=EB=A7=88=EC=BC=93=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=9C=A0=ED=8B=B8=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - market/web intent URI prefix를 상수로 분리해 가독성 향상 - Google Play 앱 패키지명을 명시하여 의도된 마켓 앱으로 이동하도록 지정 --- .../utils/appupdate/AppUpdateUtils.kt | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/appupdate/AppUpdateUtils.kt b/app/src/main/java/com/sopt/clody/presentation/utils/appupdate/AppUpdateUtils.kt index 1295fcc9..cc4b2b57 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/appupdate/AppUpdateUtils.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/appupdate/AppUpdateUtils.kt @@ -3,27 +3,41 @@ package com.sopt.clody.presentation.utils.appupdate import android.app.Activity import android.content.Context import android.content.Intent -import android.net.Uri +import android.util.Log +import androidx.core.net.toUri object AppUpdateUtils { + private const val PLAY_STORE_PACKAGE = "com.android.vending" + private const val MARKET_URI_PREFIX = "market://details?id=" + private const val WEB_URI_PREFIX = "https://play.google.com/store/apps/details?id=" + /** * 마켓 이동 * @param context Context */ fun navigateToMarket(context: Context) { val packageName = context.packageName - val marketIntent = Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName")).apply { + + val marketIntent = Intent(Intent.ACTION_VIEW, "$MARKET_URI_PREFIX$packageName".toUri()).apply { + setPackage(PLAY_STORE_PACKAGE) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + val webIntent = Intent(Intent.ACTION_VIEW, "$WEB_URI_PREFIX$packageName".toUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } - if (marketIntent.resolveActivity(context.packageManager) != null) { - context.startActivity(marketIntent) - } else { - val webIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$packageName")).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + runCatching { + val safeIntent = if (marketIntent.resolveActivity(context.packageManager) != null) { + marketIntent + } else { + webIntent } - context.startActivity(webIntent) + context.startActivity(safeIntent) + }.onFailure { + // 예외 상황 처리 (마켓 앱도, 브라우저도 없는 극단적 상황 과연 있을까?) + Log.e("AppUpdateUtils", "Failed to open market", it) } } From 397f627b7cf9350b5dce95aef070eebba47038fa Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 22 Jun 2025 01:40:40 +0900 Subject: [PATCH 191/299] =?UTF-8?q?[FEAT/#285]=20WebView=20=EB=B3=B4?= =?UTF-8?q?=EC=95=88=20=EA=B0=95=ED=99=94=20=EC=A0=81=EC=9A=A9(=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=ED=95=84=ED=84=B0=EB=A7=81=20=EB=B0=8F=20?= =?UTF-8?q?SSL=20=EC=84=A4=EC=A0=95=20=EA=B0=9C=EC=84=A0)=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B3=84=EB=8F=84=20=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/webview/WebViewNavGraph.kt | 19 +++++++++++++++ .../screen => webview}/WebViewScreen.kt | 24 ++++++++++++------- 2 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewNavGraph.kt rename app/src/main/java/com/sopt/clody/presentation/ui/{setting/screen => webview}/WebViewScreen.kt (77%) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewNavGraph.kt b/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewNavGraph.kt new file mode 100644 index 00000000..8c20e749 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewNavGraph.kt @@ -0,0 +1,19 @@ +package com.sopt.clody.presentation.ui.webview + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.sopt.clody.presentation.utils.navigation.Route + +fun NavGraphBuilder.webViewScreen( + navigateToPrevious: () -> Unit, +) { + composable { backStackEntry -> + backStackEntry.toRoute().apply { + WebViewRoute( + encodedUrl = encodedUrl, + navigateToPrevious = navigateToPrevious, + ) + } + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/WebViewScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewScreen.kt similarity index 77% rename from app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/WebViewScreen.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewScreen.kt index c2e12e04..0515fc59 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/WebViewScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewScreen.kt @@ -1,9 +1,9 @@ -package com.sopt.clody.presentation.ui.setting.screen +package com.sopt.clody.presentation.ui.webview import android.annotation.SuppressLint +import android.net.Uri import android.webkit.WebSettings import android.webkit.WebView -import android.webkit.WebViewClient import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -16,6 +16,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView +import com.sopt.clody.BuildConfig +import com.sopt.clody.core.security.weview.SecureWebViewClient @Composable fun WebViewRoute( @@ -34,27 +36,33 @@ fun WebViewScreen( encodedUrl: String, onClickBack: () -> Unit, ) { + val decodedUrl = remember(encodedUrl) { + Uri.decode(encodedUrl) + } + var webView: WebView? by remember { mutableStateOf(null) } val canGoBack by remember { derivedStateOf { webView?.canGoBack() ?: false } } + val allowedDomains = BuildConfig.ALLOWED_WEBVIEW_DOMAINS.split(",").map { it.trim() } + Scaffold( modifier = Modifier.fillMaxSize(), content = { innerPadding -> AndroidView( factory = { context -> WebView(context).apply { - webViewClient = WebViewClient() + webViewClient = SecureWebViewClient(context, allowedDomains) settings.apply { javaScriptEnabled = true domStorageEnabled = true useWideViewPort = true loadWithOverviewMode = true - allowFileAccess = true - allowContentAccess = true - javaScriptCanOpenWindowsAutomatically = true - mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + allowFileAccess = false + allowContentAccess = false + javaScriptCanOpenWindowsAutomatically = false + mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW } - loadUrl(encodedUrl) + loadUrl(decodedUrl) webView = this } }, From b7be7e55bff0d7b39501f152c466b34e9ff22132 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 22 Jun 2025 01:42:26 +0900 Subject: [PATCH 192/299] =?UTF-8?q?[REFACTOR/#285]=20WebView=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=98=B5=EC=85=98=20=EB=A7=81=ED=81=AC=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/signup/SignUpContract.kt | 3 +- .../ui/auth/signup/SignUpScreen.kt | 10 +++- .../ui/auth/signup/SignUpViewModel.kt | 5 ++ .../signup/navigation/SignUpNavigation.kt | 3 +- .../ui/auth/signup/page/TermsOfServicePage.kt | 9 ++-- .../presentation/ui/main/ClodyNavHost.kt | 3 +- .../setting/navigation/SettingNavigation.kt | 15 ------ .../ui/setting/screen/SettingScreen.kt | 51 +++++-------------- 8 files changed, 37 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt index 4e9d003d..501a0140 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt @@ -33,12 +33,13 @@ class SignUpContract { data class ToggleAllChecked(val checked: Boolean) : SignUpIntent() data class ToggleServiceChecked(val checked: Boolean) : SignUpIntent() data class TogglePrivacyChecked(val checked: Boolean) : SignUpIntent() - + data class OpenWebView(val url: String) : SignUpIntent() data object BackToTerms : SignUpIntent() } sealed interface SignUpSideEffect { data object NavigateToTimeReminder : SignUpSideEffect + data class NavigateToWebView(val url: String) : SignUpSideEffect data class ShowMessage(val message: String) : SignUpSideEffect } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt index c249b882..fb970fd3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt @@ -18,6 +18,7 @@ fun SignUpRoute( viewModel: SignUpViewModel = mavericksViewModel(), navigateToHome: () -> Unit, navigateToPrevious: () -> Unit, + navigateToWebView: (String) -> Unit, ) { val state by viewModel.collectAsState() val context = LocalContext.current @@ -29,7 +30,11 @@ fun SignUpRoute( when (effect) { is SignUpContract.SignUpSideEffect.NavigateToTimeReminder -> navigateToHome() is SignUpContract.SignUpSideEffect.ShowMessage -> { - // 삐용삐용 에러 대응을 어떻게 할까요? + // TODO: Snackbar나 Dialog로 에러 메시지 처리 + } + + is SignUpContract.SignUpSideEffect.NavigateToWebView -> { + navigateToWebView(effect.url) // ✅ WebView 이동 처리 } } } @@ -68,6 +73,9 @@ fun SignUpScreen( onTogglePrivacy = { onIntent(SignUpContract.SignUpIntent.TogglePrivacyChecked(it)) }, onAgreeClick = { onIntent(SignUpContract.SignUpIntent.ProceedTerms) }, navigateToPrevious = navigateToPrevious, + navigateToWebView = { url -> + onIntent(SignUpContract.SignUpIntent.OpenWebView(url)) + }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index 61ef7585..feeffc5b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -57,6 +57,7 @@ class SignUpViewModel @AssistedInject constructor( is SignUpContract.SignUpIntent.ToggleAllChecked -> handleToggleAllChecked(intent) is SignUpContract.SignUpIntent.ToggleServiceChecked -> handleToggleServiceChecked(intent) is SignUpContract.SignUpIntent.TogglePrivacyChecked -> handleTogglePrivacyChecked(intent) + is SignUpContract.SignUpIntent.OpenWebView -> handleOpenWebView(intent.url) SignUpContract.SignUpIntent.BackToTerms -> handleBackToTerms() } } @@ -102,6 +103,10 @@ class SignUpViewModel @AssistedInject constructor( setState { copy(privacyChecked = intent.checked) } } + private suspend fun handleOpenWebView(url: String) { + _sideEffects.send(SignUpContract.SignUpSideEffect.NavigateToWebView(url)) + } + private fun handleBackToTerms() { setState { copy( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt index 5c921424..42ea17b8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt @@ -10,15 +10,16 @@ import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.signUpScreen( navigateToHome: () -> Unit, navigateToPrevious: () -> Unit, + navigateToWebView: (String) -> Unit, ) { composable { SignUpRoute( navigateToHome = navigateToHome, navigateToPrevious = navigateToPrevious, + navigateToWebView = navigateToWebView, ) } } - fun NavController.navigateToSignUp( navOptions: NavOptionsBuilder.() -> Unit = {}, ) { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt index 31d46fcd..232b6af8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt @@ -19,7 +19,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -28,7 +27,6 @@ import com.sopt.clody.presentation.ui.auth.component.checkbox.CustomCheckbox import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls -import com.sopt.clody.presentation.ui.setting.screen.onClickSettingOption import com.sopt.clody.presentation.utils.base.BasePreview import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage @@ -44,8 +42,8 @@ fun TermsOfServicePage( onTogglePrivacy: (Boolean) -> Unit, onAgreeClick: () -> Unit, navigateToPrevious: () -> Unit, + navigateToWebView: (String) -> Unit, ) { - val context = LocalContext.current val isAgreeButtonEnabled = serviceChecked && privacyChecked Scaffold( @@ -114,14 +112,14 @@ fun TermsOfServicePage( text = stringResource(R.string.terms_service_use), checked = serviceChecked, onCheckedChange = onToggleService, - onClickMore = { onClickSettingOption(context, SettingOptionUrls.TERMS_OF_SERVICE_URL) }, + onClickMore = { navigateToWebView(SettingOptionUrls.TERMS_OF_SERVICE_URL) }, ) Spacer(modifier = Modifier.height(8.dp)) TermsCheckboxRow( text = stringResource(R.string.terms_service_privacy), checked = privacyChecked, onCheckedChange = onTogglePrivacy, - onClickMore = { onClickSettingOption(context, SettingOptionUrls.PRIVACY_POLICY_URL) }, + onClickMore = { navigateToWebView(SettingOptionUrls.PRIVACY_POLICY_URL) }, ) } }, @@ -171,6 +169,7 @@ private fun TermsOfServicePagePreview() { onTogglePrivacy = {}, onAgreeClick = {}, navigateToPrevious = {}, + navigateToWebView = {}, ) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt index 428f2c2b..b8fcbb80 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt @@ -30,8 +30,8 @@ import com.sopt.clody.presentation.ui.setting.navigation.navigateToSetting import com.sopt.clody.presentation.ui.setting.navigation.navigateToWebView import com.sopt.clody.presentation.ui.setting.navigation.notificationSettingScreen import com.sopt.clody.presentation.ui.setting.navigation.settingScreen -import com.sopt.clody.presentation.ui.setting.navigation.webViewScreen import com.sopt.clody.presentation.ui.splash.navigation.splashScreen +import com.sopt.clody.presentation.ui.webview.webViewScreen import com.sopt.clody.presentation.ui.writediary.navigation.navigateToWriteDiary import com.sopt.clody.presentation.ui.writediary.navigation.writeDiaryScreen import com.sopt.clody.presentation.utils.navigation.safePopBackStack @@ -73,6 +73,7 @@ fun ClodyNavHost( signUpScreen( navigateToHome = navController::navigateToTimeReminder, navigateToPrevious = navController::safePopBackStack, + navigateToWebView = navController::navigateToWebView, ) timeReminderScreen( navigateToGuide = navController::navigateToGuide, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt index e2228bc8..572e088c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt @@ -4,11 +4,9 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable -import androidx.navigation.toRoute import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationSettingRoute import com.sopt.clody.presentation.ui.setting.screen.AccountManagementRoute import com.sopt.clody.presentation.ui.setting.screen.SettingRoute -import com.sopt.clody.presentation.ui.setting.screen.WebViewRoute import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.settingScreen( @@ -47,19 +45,6 @@ fun NavGraphBuilder.notificationSettingScreen( } } -fun NavGraphBuilder.webViewScreen( - navigateToPrevious: () -> Unit, -) { - composable { backStackEntry -> - backStackEntry.toRoute().apply { - WebViewRoute( - encodedUrl = encodedUrl, - navigateToPrevious = navigateToPrevious, - ) - } - } -} - fun NavController.navigateToSetting( navOptions: NavOptionsBuilder.() -> Unit = {}, ) { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt index 13edf99e..d86ea0bb 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt @@ -1,8 +1,5 @@ package com.sopt.clody.presentation.ui.setting.screen -import android.content.Context -import android.content.Intent -import android.net.Uri import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api @@ -13,7 +10,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R @@ -45,7 +41,10 @@ fun SettingRoute( onClickBack = navigateToPrevious, onClickAccountManagement = navigateToAccountManagement, onClickNotificationSetting = navigateToNotification, + onClickAnnouncement = { navigateToWebView(SettingOptionUrls.ANNOUNCEMENT_URL) }, onClickInquiriesSuggestions = { navigateToWebView(SettingOptionUrls.INQUIRIES_SUGGESTIONS_URL) }, + onClickTerms = { navigateToWebView(SettingOptionUrls.TERMS_OF_SERVICE_URL) }, + onClickPrivacy = { navigateToWebView(SettingOptionUrls.PRIVACY_POLICY_URL) }, ) } @@ -56,21 +55,19 @@ fun SettingScreen( onClickBack: () -> Unit, onClickAccountManagement: () -> Unit, onClickNotificationSetting: () -> Unit, + onClickAnnouncement: () -> Unit, onClickInquiriesSuggestions: () -> Unit, + onClickTerms: () -> Unit, + onClickPrivacy: () -> Unit, ) { val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val context = LocalContext.current Scaffold( - modifier = Modifier - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { SettingTopAppBar(stringResource(R.string.setting_title), onClickBack) }, containerColor = ClodyTheme.colors.white, ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding), - ) { + Column(modifier = Modifier.padding(innerPadding)) { SettingOption(option = stringResource(R.string.setting_option_account_management), onClickAccountManagement) SettingSeparateLine() @@ -79,37 +76,15 @@ fun SettingScreen( option = stringResource(R.string.setting_option_notification_setting), onClickNotificationSetting, ) - SettingOption(option = stringResource(R.string.setting_option_announcement)) { - onClickSettingOption( - context, - SettingOptionUrls.ANNOUNCEMENT_URL, - ) - } - SettingOption( - option = stringResource(R.string.setting_option_inquiries_suggestions), - onClickInquiriesSuggestions, - ) + SettingOption(option = stringResource(R.string.setting_option_announcement), onClickAnnouncement) + SettingOption(option = stringResource(R.string.setting_option_inquiries_suggestions), onClickInquiriesSuggestions) SettingSeparateLine() - SettingOption(option = stringResource(R.string.setting_option_terms_of_service)) { - onClickSettingOption( - context, - SettingOptionUrls.TERMS_OF_SERVICE_URL, - ) - } - SettingOption(option = stringResource(R.string.setting_option_privacy_policy)) { - onClickSettingOption( - context, - SettingOptionUrls.PRIVACY_POLICY_URL, - ) - } + SettingOption(option = stringResource(R.string.setting_option_terms_of_service), onClickTerms) + SettingOption(option = stringResource(R.string.setting_option_privacy_policy), onClickPrivacy) + SettingVersionInfo(versionInfo = versionInfo) } } } - -fun onClickSettingOption(context: Context, url: String) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - context.startActivity(intent) -} From f74fec633c7d9c06aaf3e59635e3f83084ebcefb Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 22 Jun 2025 01:42:39 +0900 Subject: [PATCH 193/299] =?UTF-8?q?[ADD/#285]=20login=20error=20=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A7=81=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f5416386..b598e89f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -215,4 +215,7 @@ %1$d년 %1$d월 + + Chrome이 설치되어 있어야 로그인이 가능합니다. + 루팅된 기기에서는 로그인할 수 없습니다. From 77ab155a0eac02837925eb616fecbb608a061b88 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 25 Jun 2025 16:23:31 +0900 Subject: [PATCH 194/299] =?UTF-8?q?[MOD/#287]=20=EC=95=B1=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=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 - versionCode: 25 -> 26 - versionName: 1.1.1 -> 1.2.0 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5ea77adb..2386d33f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,8 +25,8 @@ android { applicationId = "com.sopt.clody" minSdk = 28 targetSdk = 35 - versionCode = 25 - versionName = "1.1.1" + versionCode = 26 + versionName = "1.2.0" val kakaoApiKey: String = properties.getProperty("kakao.api.key") val amplitudeApiKey: String = properties.getProperty("amplitude.api.key") val googleAdmobAppId: String = properties.getProperty("GOOGLE_ADMOB_APP_ID", "") From 0bc6659d3c7a0433e9acf6201edd2f2bb22d9895 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 29 Jun 2025 15:53:22 +0900 Subject: [PATCH 195/299] =?UTF-8?q?[REFACTOR/#285]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B3=B4=EC=95=88=20=EA=B2=80=EC=82=AC=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20@Singleton=20Annotation=20=EC=9C=84=EC=B9=98=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 --- .../clody/core/security/login/DefaultLoginSecurityChecker.kt | 2 -- .../java/com/sopt/clody/core/security/login/SecurityModule.kt | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt b/app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt index b133960c..400c45d0 100644 --- a/app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt +++ b/app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt @@ -5,7 +5,6 @@ import android.content.pm.PackageManager import android.os.Build import java.io.File import javax.inject.Inject -import javax.inject.Singleton /** * 기본 보안 점검 구현체. @@ -14,7 +13,6 @@ import javax.inject.Singleton * */ -@Singleton class DefaultLoginSecurityChecker @Inject constructor() : LoginSecurityChecker { /** diff --git a/app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt b/app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt index 89bdd485..e20c1e10 100644 --- a/app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt +++ b/app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt @@ -4,12 +4,14 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) abstract class SecurityModule { @Binds + @Singleton abstract fun bindLoginSecurityChecker( impl: DefaultLoginSecurityChecker, ): LoginSecurityChecker From 9bb686e8f4da92a9b71a61d49d556f3f11eb37aa Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sun, 29 Jun 2025 16:47:01 +0900 Subject: [PATCH 196/299] trigger CI From b34ae87077c61ca76fe61df9ec0ac4b648b527eb Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Fri, 4 Jul 2025 00:17:24 +0900 Subject: [PATCH 197/299] =?UTF-8?q?[FIX/#290]=20=EB=A6=AC=EB=A7=88?= =?UTF-8?q?=EC=9D=B8=EB=93=9C=20=EC=8B=9C=EA=B0=84=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=EA=B0=92=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timepicker/BottomSheetTimePicker.kt | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt index 032d65e8..93a25fb2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt @@ -36,6 +36,22 @@ fun BottomSheetTimePicker( onDismissRequest: () -> Unit, onRemindTimeSelected: (TimePeriod, String, String) -> Unit, ) { + val amPmEnumItems = listOf(TimePeriod.AM, TimePeriod.PM) + val amPmLabelItems = amPmEnumItems.map { it.getLabel() } + + val hourItems = remember { (1..12).map { it.toString() } } + val minuteItems = remember { listOf("00", "10", "20", "30", "40", "50") } + + val amPmPickerState = rememberPickerState().apply { + selectedItem = amPmLabelItems[1] + } + val hourPickerState = rememberPickerState().apply { + selectedItem = "9" + } + val minutePickerState = rememberPickerState().apply { + selectedItem = "30" + } + Surface( modifier = Modifier .fillMaxWidth() @@ -76,16 +92,6 @@ fun BottomSheetTimePicker( } } - val amPmEnumItems = listOf(TimePeriod.AM, TimePeriod.PM) - val amPmLabelItems = amPmEnumItems.map { it.getLabel() } - - val hourItems = remember { (1..12).map { it.toString() } } - val minuteItems = remember { listOf("00", "10", "20", "30", "40", "50") } - - val amPmPickerState = rememberPickerState() - val hourPickerState = rememberPickerState() - val minutePickerState = rememberPickerState() - Box(modifier = Modifier.fillMaxWidth()) { Box( modifier = Modifier @@ -130,7 +136,7 @@ fun BottomSheetTimePicker( ClodyButton( onClick = { val selectedLabel = amPmPickerState.selectedItem - val selectedPeriod = amPmEnumItems.getOrNull(amPmLabelItems.indexOf(selectedLabel)) ?: TimePeriod.PM + val selectedPeriod = amPmEnumItems.getOrElse(amPmLabelItems.indexOf(selectedLabel)) { TimePeriod.PM } val selectedHour = hourPickerState.selectedItem val selectedMinute = minutePickerState.selectedItem From 0bf5ae099bc591fd5e264b36afcca04e66dd882a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Fri, 4 Jul 2025 00:17:48 +0900 Subject: [PATCH 198/299] =?UTF-8?q?[MOD/#290]=20=ED=83=80=EC=9E=84?= =?UTF-8?q?=ED=94=BC=EC=BB=A4=20=EC=A0=9C=EB=AA=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b598e89f..5dd2e564 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,7 +31,7 @@ 다음 - 다른 시간 보기 + 발송 시간 변경 몇시에 감사일기\n작성 알림을 드릴까요? From b50bc46377df307466e1e9fea7d7f1506ca30229 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Fri, 4 Jul 2025 00:19:49 +0900 Subject: [PATCH 199/299] =?UTF-8?q?[FIX/#290]=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B0=8F=20=EB=B2=84=EC=A0=84=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=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 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f6eb848b..e43c8252 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,8 +25,8 @@ android { applicationId = "com.sopt.clody" minSdk = 28 targetSdk = 35 - versionCode = 26 - versionName = "1.2.0" + versionCode = 27 + versionName = "1.3.0" val kakaoApiKey: String = properties.getProperty("kakao.api.key") val amplitudeApiKey: String = properties.getProperty("amplitude.api.key") val googleAdmobAppId: String = properties.getProperty("GOOGLE_ADMOB_APP_ID", "") From 2bb738f8ea074f22ba5696d1451542a1ffdac093 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 5 Jul 2025 16:19:34 +0900 Subject: [PATCH 200/299] =?UTF-8?q?[FEAT/#293]=20=EC=8A=A4=ED=94=8C?= =?UTF-8?q?=EB=9E=98=EC=8B=9C=20=ED=99=94=EB=A9=B4,=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=ED=98=84=EC=A7=80=ED=99=94?= =?UTF-8?q?=EB=A5=BC=20=EC=A7=84=ED=96=89=ED=95=A9=EB=8B=88=EB=8B=A4.=20-?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=99=94=EB=A9=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=8A=A4=ED=94=8C=EB=9E=98=EC=8B=9C=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EA=B3=BC=20=EB=8F=99=EC=9D=BC=ED=95=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=97=90=EC=85=8B=EC=9D=84=20=ED=99=9C?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EC=9E=91=ED=95=98=EA=B3=A0=20=EC=98=81=EC=96=B4=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=EC=9D=B4=EB=A9=B4=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC=EC=9D=84=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/component/button/GoogleButton.kt | 68 ++++++++++++++++++ .../presentation/ui/login/LoginContract.kt | 1 + .../presentation/ui/login/LoginScreen.kt | 55 ++++++++------ .../presentation/ui/login/LoginViewModel.kt | 4 ++ .../main/res/drawable-en/img_splash_logo.png | Bin 0 -> 17236 bytes .../res/drawable/img_google_button_logo.png | Bin 0 -> 3730 bytes app/src/main/res/values/strings.xml | 1 + 7 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt create mode 100644 app/src/main/res/drawable-en/img_splash_logo.png create mode 100644 app/src/main/res/drawable/img_google_button_logo.png diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt new file mode 100644 index 00000000..9f9cddc3 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt @@ -0,0 +1,68 @@ +package com.sopt.clody.presentation.ui.auth.component.button + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +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.sopt.clody.R +import com.sopt.clody.ui.theme.ClodyTheme + +@Composable +fun GoogleButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + onClick = onClick, + colors = ButtonDefaults.buttonColors(containerColor = ClodyTheme.colors.gray08), + shape = RoundedCornerShape(10.dp), + modifier = modifier + .fillMaxWidth() + .height(48.dp) + .padding(horizontal = 24.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize(), + ) { + Image( + painter = painterResource(id = R.drawable.img_google_button_logo), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = text, + style = ClodyTheme.typography.body2SemiBold, + color = ClodyTheme.colors.gray01, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun GoogleButtonPreview() { + GoogleButton( + text = "Sign Up With Google", + onClick = { /*TODO*/ }, + ) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt index 38b71267..4e7d228d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt @@ -12,6 +12,7 @@ class LoginContract { sealed class LoginIntent { data class LoginWithKakao(val context: Context) : LoginIntent() + data class LoginWithGoogle(val context: Context) : LoginIntent() data object ClearError : LoginIntent() } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt index b9a5d4ab..e01b25d2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt @@ -8,13 +8,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue 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 @@ -32,6 +32,7 @@ import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage import com.sopt.clody.presentation.utils.extension.repeatOnStarted import com.sopt.clody.ui.theme.ClodyTheme +import java.util.Locale @Composable fun LoginRoute( @@ -59,9 +60,8 @@ fun LoginRoute( LoginScreen( isLoading = state.isLoading, - onLoginClick = { - viewModel.postIntent(LoginContract.LoginIntent.LoginWithKakao(context)) - }, + onKaKaoLoginClick = { viewModel.postIntent(LoginContract.LoginIntent.LoginWithKakao(context)) }, + onGoogleLoginClick = { viewModel.postIntent(LoginContract.LoginIntent.LoginWithGoogle(context)) }, ) // 에러 메시지 추가로 다이얼로그로 처리하고 싶다면? @@ -75,10 +75,12 @@ fun LoginRoute( @Composable fun LoginScreen( isLoading: Boolean, - onLoginClick: () -> Unit, + onKaKaoLoginClick: () -> Unit, + onGoogleLoginClick: () -> Unit, ) { val systemUiController = rememberSystemUiController() val backgroundColor = ClodyTheme.colors.white + val currentLang = Locale.getDefault().language LaunchedEffect(Unit) { systemUiController.setStatusBarColor( @@ -89,9 +91,28 @@ fun LoginScreen( Scaffold( bottomBar = { +// if (currentLang == "en") { +// GoogleButton( +// text = stringResource(R.string.signup_btn_google), +// onClick = onGoogleLoginClick, +// modifier = Modifier +// .fillMaxWidth() +// .navigationBarsPadding() +// .padding(bottom = 40.dp), +// ) +// } else { +// KaKaoButton( +// text = stringResource(id = R.string.signup_btn_kakao), +// onClick = onKaKaoLoginClick, +// modifier = Modifier +// .fillMaxWidth() +// .navigationBarsPadding() +// .padding(bottom = 40.dp), +// ) +// } KaKaoButton( text = stringResource(id = R.string.signup_btn_kakao), - onClick = onLoginClick, + onClick = onKaKaoLoginClick, modifier = Modifier .fillMaxWidth() .navigationBarsPadding() @@ -106,22 +127,11 @@ fun LoginScreen( .padding(innerPadding), horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(modifier = Modifier.heightForScreenPercentage(0.38f)) - Image( - painter = painterResource(id = R.drawable.ic_signup_logo), - contentDescription = null, - contentScale = ContentScale.Crop, - ) - Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) - Image( - painter = painterResource(id = R.drawable.ic__signup_title), - contentDescription = null, - ) - Spacer(modifier = Modifier.heightForScreenPercentage(0.01f)) + Spacer(modifier = Modifier.heightForScreenPercentage(0.36f)) Image( - painter = painterResource(id = R.drawable.ic_signup_logotitle), - contentDescription = null, - contentScale = ContentScale.Crop, + painter = painterResource(id = R.drawable.img_splash_logo), + contentDescription = "App Logo", + modifier = Modifier.size(160.dp), ) } } @@ -137,7 +147,8 @@ fun LoginScreenPreview() { BasePreview { LoginScreen( isLoading = false, - onLoginClick = {}, + onKaKaoLoginClick = {}, + onGoogleLoginClick = {}, ) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt index b16060ee..9f058f24 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -46,6 +46,7 @@ class LoginViewModel @AssistedInject constructor( private suspend fun handleIntent(intent: LoginContract.LoginIntent) { when (intent) { is LoginContract.LoginIntent.LoginWithKakao -> loginWithKakao(intent.context) + is LoginContract.LoginIntent.LoginWithGoogle -> loginWithGoogle(intent.context) is LoginContract.LoginIntent.ClearError -> setState { copy(errorMessage = null) } } } @@ -91,6 +92,9 @@ class LoginViewModel @AssistedInject constructor( ) } + private suspend fun loginWithGoogle(context: Context) { + } + @AssistedFactory interface Factory : AssistedViewModelFactory { override fun create(state: LoginContract.LoginState): LoginViewModel diff --git a/app/src/main/res/drawable-en/img_splash_logo.png b/app/src/main/res/drawable-en/img_splash_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..81b70d4799e82b1f3a41cec2fecc91886590c71a GIT binary patch literal 17236 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

%7;MjWDOcm^Lywp7P$)nV5!or&-Zv-6ofv zQ%A#xcl1bX?#l9}Ze|+lEYms^0Wrsy+5Q{4E{2~-JzbOgs}12Fy2W*NtjrgB?jO?6 z9v72p+7{OpHO$`5d`H*#b4yiXBd6`{$yU>|qri|0dg*9SJhoC@AvY8Z!zqkK*b#fp z4Z!e3LtpdW5ea6}wFRPUV`=H1W}T*oUvD^I^QebZSa$?Bwc~BFC(QvJqBT8fz4O@wH4b+cs?FnMqPp zGlw?A5}pAM(R9YiDb9pRFn^ll<6+q*fn%BtEW@=h%)o(n^@`^>E~p;y4G)YNUbe8u z1dV5H=$+Xm~u4tR6?xWlV3zr2Oz zJyJ6>W(o>*2{BV#l9PU<+CPG2`%^QQ)9ET$A1r5&1Y`fl(UNUmWdHxj2TMDSZo)H# z9gzQoi}^X!={XglbWJ||uQyJ9P#4wZ!fq$^3F|-7h}59UR$89?&42HrSSF1n`Bi!s zq!P*gN4qnn&;^x;6Nt(FA7QaTBA_}>m$j6${CEGo@n_1;K3=Q%*T3tJ^u!nzEI|{X z`2p4mYRmf#-v5L%wTr&!hv7kVSV{;G&UT}AkUC(N6E-lrWBS}5;b0&QcH@p>bis#7 z#>4aB)Bgy50m}!nV}TtJ@Bh&yu#Ow(RKl>*zabXY%bf2pgxwPV@fWI3JL)Qa`o&1~ zuc^~k;##jOO&YpTZH!3=3+D!a;SH3)4hNuxgnv0HK#aLiceq2Ar~e~^B+D}~NayK) z25&UHCF#g0?EjXAREiY}42CqJuK&97l@FX2HLCPIE@TajisJTub%FZ+%Y9XzFO80X z<-V?TDKQHDlOlWd_0IVAhmKlMIoNHymLVBzSDGqxT=~By&;y)+=6d{}-EQ`)mrf@| SCjk$^VDNPHb6Mw<&;$VC5^^a3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/img_google_button_logo.png b/app/src/main/res/drawable/img_google_button_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..dfb8a7478d1cb3129257fbffd1b4f4519e67673c GIT binary patch literal 3730 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5dd2e564..f53e8905 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ 행운을 전하는 감사일기 카카오로 로그인 + Sign Up With Google From cebeb74d87e7f4e1546ef7f631905ce6acb96f18 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 9 Jul 2025 16:26:51 +0900 Subject: [PATCH 201/299] =?UTF-8?q?[FEAT/#293]=20=EC=98=A8=EB=B3=B4?= =?UTF-8?q?=EB=94=A9=20=ED=99=94=EB=A9=B4,=20=EA=B0=80=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=ED=98=84=EC=A7=80=ED=99=94=EB=A5=BC=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20=EC=95=BD?= =?UTF-8?q?=EA=B4=80=EB=8F=99=EC=9D=98=20=ED=99=94=EB=A9=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9A=94=EC=86=8C=20=EA=B0=84=20=EA=B0=84=EA=B2=A9?= =?UTF-8?q?=20=EC=A1=B0=EC=A0=88=20-=20=EA=B0=80=EC=9D=B4=EB=93=9C=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20title,=20subtitle?= =?UTF-8?q?=EC=9D=84=20=ED=95=98=EB=82=98=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timepicker/BottomSheetTimePicker.kt | 2 +- .../presentation/ui/auth/guide/GuideScreen.kt | 13 ------- .../ui/auth/signup/page/TermsOfServicePage.kt | 4 +-- .../NotificationSettingTimePicker.kt | 2 +- app/src/main/res/values-en/strings.xml | 36 +++++++++++++++++++ app/src/main/res/values/strings.xml | 26 ++++++-------- 6 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 app/src/main/res/values-en/strings.xml diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt index 93a25fb2..175232d5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt @@ -73,7 +73,7 @@ fun BottomSheetTimePicker( .padding(top = 16.dp, bottom = 30.dp), ) { Text( - stringResource(id = R.string.time_picker_title), + stringResource(id = R.string.time_reminder_picker_title), style = ClodyTheme.typography.head4, color = ClodyTheme.colors.gray01, modifier = Modifier.align(Alignment.Center), diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt index 6611a7b8..75a736f1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt @@ -3,7 +3,6 @@ package com.sopt.clody.presentation.ui.auth.guide import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeOut -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -51,7 +50,6 @@ fun GuideRoute( GuideScreen(onNextButtonClick = navigateToHome) } -@OptIn(ExperimentalFoundationApi::class) @Composable fun GuideScreen( onNextButtonClick: () -> Unit, @@ -59,25 +57,21 @@ fun GuideScreen( val pages = listOf( BoardingPage( title = stringResource(R.string.guide_page1_title), - subtitle = stringResource(R.string.guide_page1_subtitle), description = stringResource(R.string.guide_page1_description), imageRes = R.drawable.img_guide_first, ), BoardingPage( title = stringResource(R.string.guide_page2_title), - subtitle = stringResource(R.string.guide_page2_subtitle), description = stringResource(R.string.guide_page2_description), imageRes = R.drawable.img_guide_second, ), BoardingPage( title = stringResource(R.string.guide_page3_title), - subtitle = stringResource(R.string.guide_page3_subtitle), description = stringResource(R.string.guide_page3_description), imageRes = R.drawable.img_guide_third, ), BoardingPage( title = stringResource(R.string.guide_page4_title), - subtitle = stringResource(R.string.guide_page4_subtitle), description = stringResource(R.string.guide_page4_description), imageRes = R.drawable.img_guide_fourth, ), @@ -140,12 +134,6 @@ fun GuideScreen( color = ClodyTheme.colors.gray01, textAlign = TextAlign.Center, ) - Text( - text = pages[page].subtitle, - style = ClodyTheme.typography.head1, - color = ClodyTheme.colors.gray01, - textAlign = TextAlign.Center, - ) Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) Text( text = pages[page].description, @@ -194,7 +182,6 @@ fun GuideScreen( data class BoardingPage( val title: String, - val subtitle: String, val description: String, val imageRes: Int, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt index 232b6af8..620749de 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt @@ -105,7 +105,7 @@ fun TermsOfServicePage( uncheckedImageRes = R.drawable.ic_terms_check_off_25, ) } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(18.dp)) HorizontalDivider(color = ClodyTheme.colors.gray07, thickness = 1.dp) Spacer(modifier = Modifier.height(16.dp)) TermsCheckboxRow( @@ -114,7 +114,7 @@ fun TermsOfServicePage( onCheckedChange = onToggleService, onClickMore = { navigateToWebView(SettingOptionUrls.TERMS_OF_SERVICE_URL) }, ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(24.dp)) TermsCheckboxRow( text = stringResource(R.string.terms_service_privacy), checked = privacyChecked, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt index b7b52f80..0ed6cf5e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt @@ -66,7 +66,7 @@ fun NotificationSettingTimePicker( .padding(top = 16.dp, bottom = 30.dp), ) { Text( - stringResource(id = R.string.time_picker_title), + stringResource(id = R.string.time_reminder_picker_title), style = ClodyTheme.typography.head4, color = ClodyTheme.colors.gray01, modifier = Modifier.align(Alignment.Center), diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml new file mode 100644 index 00000000..e33001f5 --- /dev/null +++ b/app/src/main/res/values-en/strings.xml @@ -0,0 +1,36 @@ + + + Clody + + Sign Up With Google + + To use Clody,\nPlease agree to the terms\nand conditions + Agree to all + (Required) Terms of Service + (Required) Privacy Policy + Next + + Nice to meet you!\nWhat should I call you? + Please enter your nickname + Next + + What time would you\nlike us to remind you to write? + Change reminder time + Save + Skip for now + + Hello!\nI\'m Lody + I read your journal entries\nand reply with compliments\nand words of encouragement! + + Each reply comes\nwith a lucky four-leaf clover + The more you confide in Lody\nthe luckier your clover becomes + + You can only journal\nfor today and yesterday + Past or future dates aren\'t available,\nDon\'t miss your moment + + Ready to write\nyour first journal?\nI\'ll be waiting! + From your second journal onward,\nyour next clover will take 12 hours to grow + + Next + Get Started + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f53e8905..ff191db7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,11 +19,6 @@ Sign Up With Google - - 만나서 반가워요!\n어떻게 불러 드릴까요? - 닉네임을 입력해주세요 - 다음 - "Clody 이용을 위해\n약관에 동의해 주세요" 전체 동의하기 @@ -31,29 +26,28 @@ (필수) 개인정보 처리방침 다음 - - 발송 시간 변경 + + 만나서 반가워요!\n어떻게 불러 드릴까요? + 닉네임을 입력해주세요 + 다음 - + 몇시에 감사일기\n작성 알림을 드릴까요? 다음에 설정할게요 + 발송 시간 변경 완료 - 안녕하세요! - 저는 로디라고 해요 + 안녕하세요!\n저는 로디라고 해요 여러분이 써준 감사일기를 받고,\n칭찬과 응원을 담아 답장을 쓴답니다 - 답장마다 행운의 - 네잎클로버를 함께 드려요 + 답장마다 행운의\n네잎클로버를 함께 드려요 하루에 받은 감사의 수가 많을수록\n색이 진한 네잎클로버를 전달해요 - 오늘과 전날 일기만 - 작성할 수 있어요 + 오늘과 전날 일기만\n작성할 수 있어요 그전이나 다음날의 일기는 작성할 수\n없으니, 잊지 말고 기록해주세요 - 이제 일기를 써볼까요? - 기다리고 있을게요! + 이제 일기를 써볼까요?\n기다리고 있을게요! 두번째 일기부터는 네잎클로버를 찾는 데\n12시간이 걸리니 조금만 기다려 주세요 다음 From e804505c2898f2044d3ab6d88f5e2cc58ff95abf Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 9 Jul 2025 23:57:05 +0900 Subject: [PATCH 202/299] =?UTF-8?q?[FEAT/#293]=20=EC=95=B1=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=ED=98=84=EC=A7=80=ED=99=94=EB=A5=BC=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values-en/strings.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index e33001f5..080aaecc 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -2,6 +2,15 @@ Clody + Update Available + A new version %1$s is available. Do you want to update now? + Update + Later + Update Required + Please update to version %1$s to continue + Exit App + + Sign Up With KaKao Sign Up With Google To use Clody,\nPlease agree to the terms\nand conditions From ea1f2aa3fd7f0fe30a1db369a8ca87e9d3408e66 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 10 Jul 2025 00:31:49 +0900 Subject: [PATCH 203/299] =?UTF-8?q?[DEL/#293]=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EB=A6=AC=EC=86=8C=EC=8A=A4=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=ED=95=A9=EB=8B=88=EB=8B=A4.=20(=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EB=A1=9C=20=ED=99=9C=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EC=9E=88=EB=8A=94=20=EB=B6=80=EB=B6=84=EC=9E=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ff191db7..0fd185cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,7 +14,6 @@ 앱 종료 - 행운을 전하는 감사일기 카카오로 로그인 Sign Up With Google From 0a301542ff223ac0bea84f20399fb648dc4e0b71 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 10 Jul 2025 10:56:32 +0900 Subject: [PATCH 204/299] =?UTF-8?q?[FEAT/#293]=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4,=20=EC=95=8C=EB=A6=BC=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=98=84=EC=A7=80=ED=99=94=EB=A5=BC=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20=EC=9B=B9?= =?UTF-8?q?=EB=B7=B0=20=ED=95=9C=EA=B5=AD=EC=96=B4=20=EB=A7=81=ED=81=AC,?= =?UTF-8?q?=20=EC=98=81=EC=96=B4=20=EB=A7=81=ED=81=AC=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=20(=EA=B8=B0=EB=B3=B8=20:=20=EC=98=81=EC=96=B4=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC)=20-=20=EC=98=81=EC=96=B4=20=EB=B2=88=EC=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/signup/page/TermsOfServicePage.kt | 9 +++- .../NotificationSettingTimePicker.kt | 4 +- .../ui/setting/screen/SettingOptionUrls.kt | 27 +++++++++--- .../ui/setting/screen/SettingScreen.kt | 30 ++++++++------ app/src/main/res/values-en/strings.xml | 41 +++++++++++++++++++ app/src/main/res/values/strings.xml | 9 ++-- 6 files changed, 94 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt index 620749de..379cf831 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt @@ -31,6 +31,7 @@ import com.sopt.clody.presentation.utils.base.BasePreview import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage import com.sopt.clody.ui.theme.ClodyTheme +import java.util.Locale @Composable fun TermsOfServicePage( @@ -45,6 +46,10 @@ fun TermsOfServicePage( navigateToWebView: (String) -> Unit, ) { val isAgreeButtonEnabled = serviceChecked && privacyChecked + val currentLang = Locale.getDefault().language + + val termsOfService = if (currentLang == "ko") SettingOptionUrls.TERMS_OF_SERVICE_URL.krUrl else SettingOptionUrls.TERMS_OF_SERVICE_URL.enUrl + val privacyPolicy = if (currentLang == "ko") SettingOptionUrls.PRIVACY_POLICY_URL.krUrl else SettingOptionUrls.PRIVACY_POLICY_URL.enUrl Scaffold( topBar = { @@ -112,14 +117,14 @@ fun TermsOfServicePage( text = stringResource(R.string.terms_service_use), checked = serviceChecked, onCheckedChange = onToggleService, - onClickMore = { navigateToWebView(SettingOptionUrls.TERMS_OF_SERVICE_URL) }, + onClickMore = { navigateToWebView(termsOfService) }, ) Spacer(modifier = Modifier.height(24.dp)) TermsCheckboxRow( text = stringResource(R.string.terms_service_privacy), checked = privacyChecked, onCheckedChange = onTogglePrivacy, - onClickMore = { navigateToWebView(SettingOptionUrls.PRIVACY_POLICY_URL) }, + onClickMore = { navigateToWebView(privacyPolicy) }, ) } }, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt index 0ed6cf5e..801a019a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt @@ -66,7 +66,7 @@ fun NotificationSettingTimePicker( .padding(top = 16.dp, bottom = 30.dp), ) { Text( - stringResource(id = R.string.time_reminder_picker_title), + text = stringResource(R.string.notification_setting_picker_title), style = ClodyTheme.typography.head4, color = ClodyTheme.colors.gray01, modifier = Modifier.align(Alignment.Center), @@ -144,7 +144,7 @@ fun NotificationSettingTimePicker( ).to24HourFormat() onConfirm(selectedTime) }, - text = stringResource(R.string.notification_setting_timepicker_confirm), + text = stringResource(R.string.notification_setting_picker_confirm), enabled = true, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt index 1348d8e0..058b9bc9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt @@ -1,8 +1,25 @@ package com.sopt.clody.presentation.ui.setting.screen -object SettingOptionUrls { - const val ANNOUNCEMENT_URL = "https://www.notion.so/1c7e3fedb3f48029b36cf9d76c5fb6d6?pvs=21" - const val INQUIRIES_SUGGESTIONS_URL = "https://forms.gle/WnLC7VwHacufVHiv7" - const val TERMS_OF_SERVICE_URL = "https://www.notion.so/1c7e3fedb3f4802c8db1f3056c03973f?pvs=21" - const val PRIVACY_POLICY_URL = "https://www.notion.so/1c7e3fedb3f48024a334c8116255b378?pvs=21" +enum class SettingOptionUrls( + val enUrl: String, + val krUrl: String +) { + NOTICES_URL( + enUrl = "https://tropical-buckthorn-d17.notion.site/Notice-22ae3fedb3f480feb229e7dcc7a23887?source=copy_link", + krUrl = "https://www.notion.so/1c7e3fedb3f48029b36cf9d76c5fb6d6?pvs=21" + ), + SUPPORT_FEEDBACK_URL( + enUrl = "https://docs.google.com/forms/d/e/1FAIpQLSe1LJg6tYaWBY2ji3O1smCH1ux5ItbVyGVUQko-Mg609Xt9eg/viewform", + krUrl = "https://forms.gle/WnLC7VwHacufVHiv7" + ), + TERMS_OF_SERVICE_URL( + enUrl = "https://tropical-buckthorn-d17.notion.site/Clody-Terms-of-Use-22ae3fedb3f48092ace1fba817df8605?source=copy_link", + krUrl = "https://www.notion.so/1c7e3fedb3f4802c8db1f3056c03973f?pvs=21" + ), + PRIVACY_POLICY_URL( + enUrl = "https://tropical-buckthorn-d17.notion.site/Clody-Privacy-Policy-22ae3fedb3f4808ab8dcc8ba60ad6cd6?source=copy_link", + krUrl = "https://www.notion.so/1c7e3fedb3f48024a334c8116255b378?pvs=21" + ) } + + diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt index d86ea0bb..067566ea 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt @@ -20,6 +20,7 @@ import com.sopt.clody.presentation.ui.setting.component.SettingVersionInfo import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.ui.theme.ClodyTheme +import java.util.Locale @Composable fun SettingRoute( @@ -29,6 +30,12 @@ fun SettingRoute( navigateToWebView: (String) -> Unit, settingViewModel: SettingViewModel = hiltViewModel(), ) { + val currentLang = Locale.getDefault().language + val notice = if (currentLang == "ko") SettingOptionUrls.NOTICES_URL.krUrl else SettingOptionUrls.NOTICES_URL.enUrl + val supportFeedback = if (currentLang == "ko") SettingOptionUrls.SUPPORT_FEEDBACK_URL.krUrl else SettingOptionUrls.SUPPORT_FEEDBACK_URL.enUrl + val termsOfService = if (currentLang == "ko") SettingOptionUrls.TERMS_OF_SERVICE_URL.krUrl else SettingOptionUrls.TERMS_OF_SERVICE_URL.enUrl + val privacyPolicy = if (currentLang == "ko") SettingOptionUrls.PRIVACY_POLICY_URL.krUrl else SettingOptionUrls.PRIVACY_POLICY_URL.enUrl + val versionInfo by settingViewModel::versionInfo LaunchedEffect(Unit) { @@ -37,14 +44,14 @@ fun SettingRoute( } SettingScreen( - versionInfo = versionInfo ?: stringResource(R.string.setting_version_info_failure), + versionInfo = versionInfo ?: stringResource(R.string.setting_option_app_version_info_failure), onClickBack = navigateToPrevious, onClickAccountManagement = navigateToAccountManagement, onClickNotificationSetting = navigateToNotification, - onClickAnnouncement = { navigateToWebView(SettingOptionUrls.ANNOUNCEMENT_URL) }, - onClickInquiriesSuggestions = { navigateToWebView(SettingOptionUrls.INQUIRIES_SUGGESTIONS_URL) }, - onClickTerms = { navigateToWebView(SettingOptionUrls.TERMS_OF_SERVICE_URL) }, - onClickPrivacy = { navigateToWebView(SettingOptionUrls.PRIVACY_POLICY_URL) }, + onClickNotice = { navigateToWebView(notice) }, + onClickSupportFeedback = { navigateToWebView(supportFeedback) }, + onClickTerms = { navigateToWebView(termsOfService) }, + onClickPrivacy = { navigateToWebView(privacyPolicy) }, ) } @@ -55,8 +62,8 @@ fun SettingScreen( onClickBack: () -> Unit, onClickAccountManagement: () -> Unit, onClickNotificationSetting: () -> Unit, - onClickAnnouncement: () -> Unit, - onClickInquiriesSuggestions: () -> Unit, + onClickNotice: () -> Unit, + onClickSupportFeedback: () -> Unit, onClickTerms: () -> Unit, onClickPrivacy: () -> Unit, ) { @@ -72,12 +79,9 @@ fun SettingScreen( SettingSeparateLine() - SettingOption( - option = stringResource(R.string.setting_option_notification_setting), - onClickNotificationSetting, - ) - SettingOption(option = stringResource(R.string.setting_option_announcement), onClickAnnouncement) - SettingOption(option = stringResource(R.string.setting_option_inquiries_suggestions), onClickInquiriesSuggestions) + SettingOption(option = stringResource(R.string.setting_option_notification_setting), onClickNotificationSetting,) + SettingOption(option = stringResource(R.string.setting_option_announcement), onClickNotice) + SettingOption(option = stringResource(R.string.setting_option_inquiries_suggestions), onClickSupportFeedback) SettingSeparateLine() diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 080aaecc..6ad54571 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -42,4 +42,45 @@ Next Get Started + + Settings + Profile and Account + Notification + Notices + Support/Feedback + Terms of Service + Privacy Policy + Version + Fail to Fetch + + Profile and Account + " " + Edit + Please write without spaces or special characters + Save + Save Changes + Logout + Cancel + Are you sure you want to delete your account? + Withdraw + Log out now? + I\'ll be here, see you again soon! + Logout + Delete your account? + Your journals, replies, and clovers will be\\npermanently deleted and can\'t be restored. + Withdraw + Cancel + Edit Nickname + + Notification + Journal Reminder + Draft Reminder + Reminder Time + Reply Notification + Choose another time + Save + %2$s:%3$s %1$s + Save your reminder time + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0fd185cb..9dd90b50 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -155,7 +155,6 @@ 설정 - 버전 정보를 불러오는데 실패했습니다. 프로필 및 계정 관리 알림 설정 공지사항 @@ -191,16 +190,17 @@ 이어쓰기 알림 받기 알림 시간 답장 도착 알림 받기 - 완료 + 다른 시간 보기 + 완료 %1$s %2$s시 %3$s분 - 다시 시도 - 확인 알람 시간 설정을 완료했어요. 삭제하기 다른 날짜 보기 완료 + 다시 시도 + 확인 오전 @@ -212,4 +212,5 @@ Chrome이 설치되어 있어야 로그인이 가능합니다. 루팅된 기기에서는 로그인할 수 없습니다. + From b5b33212ae6f07b9ded03062fa11b5e02ec3a995 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 10 Jul 2025 11:46:32 +0900 Subject: [PATCH 205/299] =?UTF-8?q?[FEAT/#293]=20=ED=99=88=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=98=84=EC=A7=80=ED=99=94=EB=A5=BC=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20=EC=9D=BC=EB=B6=80?= =?UTF-8?q?=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20-=20=ED=99=94=EB=A9=B4=20=EB=82=B4=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4,=20=EB=8B=A4=EC=9D=B4=EC=96=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EB=93=B1=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=82=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20-=20=EB=B6=88=ED=95=84=EC=9A=94=20=EB=A6=AC?= =?UTF-8?q?=EC=86=8C=EC=8A=A4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/diarylist/screen/DiaryListScreen.kt | 8 +- .../calendar/component/DailyDiaryListItem.kt | 6 +- .../ui/home/component/CloverCount.kt | 2 +- .../ui/home/component/DiaryStateButton.kt | 10 +-- .../ui/home/component/YearAndMonthTitle.kt | 2 +- .../presentation/ui/home/screen/HomeScreen.kt | 16 ++-- app/src/main/res/values-en/strings.xml | 25 +++++- app/src/main/res/values/strings.xml | 88 ++++++++----------- 8 files changed, 84 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt index 15de33b5..cc86c8f9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt @@ -197,10 +197,10 @@ fun DiaryListScreen( if (diaryDeleteDialogState) { ClodyDialog( - titleMassage = stringResource(R.string.diary_delete_dialog_title), - descriptionMassage = stringResource(R.string.diary_delete_dialog_description), - confirmOption = stringResource(R.string.diary_delete_dialog_confirm_option), - dismissOption = stringResource(R.string.diary_delete_dialog_dismiss_option), + titleMassage = stringResource(R.string.dialog_diary_delete_title), + descriptionMassage = stringResource(R.string.dialog_diary_delete_description), + confirmOption = stringResource(R.string.dialog_diary_delete_confirm), + dismissOption = stringResource(R.string.dialog_diary_delete_dismiss), confirmAction = { onClickDiaryDelete(selectedDiaryDate.year, selectedDiaryDate.month, selectedDiaryDate.day) dismissDiaryDeleteDialog() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt index 21192e99..267f5f12 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt @@ -54,7 +54,7 @@ fun DailyDiaryListItem( ) Text( text = stringResource( - id = R.string.daily_diary_day_of_week_format, + id = R.string.home_daily_diary_day_of_week, dayOfWeek.toKoreanShortLabel(), ), style = ClodyTheme.typography.body2Medium, @@ -80,7 +80,7 @@ fun DailyDiaryListItem( .padding(vertical = 44.dp), ) { Text( - text = stringResource(R.string.daily_diary_draft_message), + text = stringResource(R.string.home_daily_diary_draft_message), style = ClodyTheme.typography.body3Regular, color = ClodyTheme.colors.gray05, textAlign = TextAlign.Center, @@ -96,7 +96,7 @@ fun DailyDiaryListItem( .padding(vertical = 44.dp), ) { Text( - text = stringResource(R.string.daily_diary_empty_message), + text = stringResource(R.string.home_daily_diary_empty_message), style = ClodyTheme.typography.body3Regular, color = ClodyTheme.colors.gray05, textAlign = TextAlign.Center, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt index ff1029ba..f5f76ac9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt @@ -18,7 +18,7 @@ import com.sopt.clody.ui.theme.ClodyTheme @Composable fun CloverCount(cloverCount: Int) { - val text = stringResource(R.string.home_clover_count_format, cloverCount) + val text = stringResource(R.string.home_total_clover, cloverCount) Box( modifier = Modifier diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt index 5b921f05..58b2453f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt @@ -31,7 +31,7 @@ fun DiaryStateButton( hasDraft -> { ClodyButton( onClick = { onClickWriteDiary(year, month, day) }, - text = stringResource(R.string.button_continue_draft), + text = stringResource(R.string.home_button_continue_draft), enabled = true, modifier = modifier, ) @@ -40,7 +40,7 @@ fun DiaryStateButton( isInvalidDraft -> { ClodyButton( onClick = { /* no-action */ }, - text = stringResource(R.string.button_check_reply), + text = stringResource(R.string.home_button_check_reply), enabled = false, modifier = modifier, disabledContainerColor = ClodyTheme.colors.gray05, @@ -51,7 +51,7 @@ fun DiaryStateButton( canReply -> { ClodyReplyButton( onClick = onClickReplyDiary, - text = stringResource(R.string.button_check_reply), + text = stringResource(R.string.home_button_check_reply), enabled = true, modifier = modifier, ) @@ -60,7 +60,7 @@ fun DiaryStateButton( canWrite -> { ClodyButton( onClick = { onClickWriteDiary(year, month, day) }, - text = stringResource(R.string.button_write_diary), + text = stringResource(R.string.home_button_write_diary), enabled = true, modifier = modifier, ) @@ -69,7 +69,7 @@ fun DiaryStateButton( else -> { ClodyButton( onClick = { onClickWriteDiary(year, month, day) }, - text = stringResource(R.string.button_write_diary), + text = stringResource(R.string.home_button_write_diary), enabled = false, modifier = modifier, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt index b9677b37..209515cd 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt @@ -23,7 +23,7 @@ fun YearAndMonthTitle( selectedYear: Int, selectedMonth: Int, ) { - val text = stringResource(R.string.home_year_and_month_format, selectedYear, selectedMonth) + val text = stringResource(R.string.home_year_month_format, selectedYear, selectedMonth) Column { Row( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 2ef9cf90..bd3ed68e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -237,10 +237,10 @@ fun HomeRoute( if (showContinueDraftDialog) { ClodyDialog( - titleMassage = stringResource(R.string.home_continue_draft_title), - descriptionMassage = stringResource(R.string.home_continue_draft_description), - confirmOption = stringResource(R.string.home_continue_draft_confirm), - dismissOption = stringResource(R.string.home_continue_draft_dismiss), + titleMassage = stringResource(R.string.home_continue_draft_dialog_title), + descriptionMassage = stringResource(R.string.home_continue_draft_dialog_description), + confirmOption = stringResource(R.string.home_continue_draft_dialog_confirm), + dismissOption = stringResource(R.string.home_continue_draft_dialog_dismiss), confirmAction = { homeViewModel.setShowContinueDraftDialog(false) val date = homeViewModel.selectedDate.value @@ -266,10 +266,10 @@ fun HomeRoute( if (showDiaryDeleteDialog) { ClodyDialog( - titleMassage = stringResource(R.string.home_delete_diary_title), - descriptionMassage = stringResource(R.string.home_delete_diary_description), - confirmOption = stringResource(R.string.home_delete_diary_confirm), - dismissOption = stringResource(R.string.home_delete_diary_dismiss), + titleMassage = stringResource(R.string.dialog_diary_delete_title), + descriptionMassage = stringResource(R.string.dialog_diary_delete_description), + confirmOption = stringResource(R.string.dialog_diary_delete_confirm), + dismissOption = stringResource(R.string.dialog_diary_delete_dismiss), confirmAction = { homeViewModel.deleteDailyDiary( selectedDiaryDate.year, diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 6ad54571..b203e0ef 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -43,6 +43,26 @@ Next Get Started + %1$d Clovers + %2$d %1$d + No gratitude entries yet. + There are existing entries in drafts. + %1$d. %2$s + %1$s + Write a Journal + See my Reply + Continue Writing + Heads up- drafts expire\\nDon\'t miss Lody\'s reply! + To receive a reminder before the reply deadline,\nconfirm notification permissions. + [Settings > Apps > Clody > Notifications] + Turn on Notifications + Skip for now + Continue writing reminders are now on! + Pick up where you left off? + This journal\'s reply time has expired. + Continue + Cancel + Settings Profile and Account Notification @@ -82,5 +102,8 @@ %2$s:%3$s %1$s Save your reminder time - + Delete this entry? + You won\'t receive another reply\nif you delete and rewrite your entry. + Delete + Cancel diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9dd90b50..63c83c77 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,6 +52,34 @@ 다음 시작하기 + + 클로버 %1$d개 + %1$d년 %2$d월 + + 작성된 감사 일기가 없어요! + 임시저장된 일기가 있어요. + %1$d. %2$s + %1$s요일 + + + 일기 쓰기 + 답장 확인 + 이어 쓰기 + + + 기한이 지나면\n로디의 답장을 받을 수 없어요! + 답장 마감 전에 일기를 이어쓸 수 있도록\n알려드리기 위해서는 알림 설정이 필요해요. + [설정 > 애플리케이션 > 클로디 > 알림 > 알림표시] + 알림 받기 + 다음에 하기 + 이어쓰기 알림 설정을 완료했어요. + + + 임시저장된 일기를 이어 쓸까요? + 답장 기한이 지나서 답장은 받을 수 없어요. + 이어쓰기 + 아니오 + 로디가 열심히 답장을 쓰고 있어요 로디가 쓴 행운의 답장이 도착했어요! @@ -96,12 +124,6 @@ 신조어, 비속어, 이모지 작성은 불가능해요 - - 정말 일기를 삭제할까요? - 아직 답장이 오지 않았거나 삭제하고\n다시 작성한 일기는 답장을 받을 수 없어요. - 삭제할래요 - 아니요 - %1$s년 %2$s월 작성된 감사일기가 없어요 @@ -109,50 +131,6 @@ /%1$s 답장 확인 - 임시저장된 일기가 있어요. - 작성된 감사 일기가 없어요! - %1$d. %2$s - %1$s요일 - - - 클로버 %1$d개 - %1$d년 %2$d월 - - - 이어 쓰기 - 답장 확인 - 일기 쓰기 - - - 데이터를 불러오는데 실패했습니다. - 알 수 없는 오류가 발생했습니다. - 일기 삭제 중 오류가 발생했습니다. - - - 기한이 지나면\n로디의 답장을 받을 수 없어요! - 답장 마감 전에 일기를 이어쓸 수 있도록\n알려드리기 위해서는 알림 설정이 필요해요. - [설정 > 애플리케이션 > 클로디 > 알림 > 알림표시] - 알림 받기 - 다음에 하기 - - - 이어쓰기 알림 설정을 완료했어요. - - - 임시저장된 일기를 이어 쓸까요? - 답장 기한이 지나서 답장은 받을 수 없어요. - 이어쓰기 - 아니오 - - - 정말 일기를 삭제할까요? - 아직 답장이 오지 않았거나 삭제하고\n다시 작성한 일기는 답장을 받을 수 없어요. - 삭제할래요 - 아니요 - - - 월 선택 - 설정 프로필 및 계정 관리 @@ -213,4 +191,14 @@ Chrome이 설치되어 있어야 로그인이 가능합니다. 루팅된 기기에서는 로그인할 수 없습니다. + + 정말 일기를 삭제할까요? + 아직 답장이 오지 않았거나 삭제하고\n다시 작성한 일기는 답장을 받을 수 없어요. + 삭제할래요 + 아니요 + + + 데이터를 불러오는데 실패했습니다. + 알 수 없는 오류가 발생했습니다. + 일기 삭제 중 오류가 발생했습니다. From 3aee4b5ca80b97dcf979e1f716ed937e2f28cf3a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Thu, 10 Jul 2025 16:10:16 +0900 Subject: [PATCH 206/299] =?UTF-8?q?[REFACTOR/#294]=20Firebase=20Remote=20C?= =?UTF-8?q?onfig=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EA=B0=84?= =?UTF-8?q?=EA=B2=A9=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fetch 간격을 10분으로 설정하여 더 자주 업데이트를 가져오도록 수정했습니다. --- .../main/java/com/sopt/clody/di/AppUpdateModule.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/di/AppUpdateModule.kt b/app/src/main/java/com/sopt/clody/di/AppUpdateModule.kt index f584941f..98fd28c3 100644 --- a/app/src/main/java/com/sopt/clody/di/AppUpdateModule.kt +++ b/app/src/main/java/com/sopt/clody/di/AppUpdateModule.kt @@ -1,6 +1,7 @@ package com.sopt.clody.di import com.google.firebase.remoteconfig.FirebaseRemoteConfig +import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings import com.sopt.clody.data.remote.appupdate.AppUpdateCheckerImpl import com.sopt.clody.data.remote.datasource.RemoteConfigDataSource import com.sopt.clody.domain.appupdate.AppUpdateChecker @@ -16,8 +17,14 @@ object AppUpdateModule { @Provides @Singleton - fun provideFirebaseRemoteConfig(): FirebaseRemoteConfig = - FirebaseRemoteConfig.getInstance() + fun provideFirebaseRemoteConfig(): FirebaseRemoteConfig { + val remoteConfig = FirebaseRemoteConfig.getInstance() + val configSettings = FirebaseRemoteConfigSettings.Builder() + .setMinimumFetchIntervalInSeconds(600L) + .build() + remoteConfig.setConfigSettingsAsync(configSettings) + return remoteConfig + } @Provides @Singleton From 0261de09cc358992e11c2aa8cdb9b631d3995f95 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Thu, 10 Jul 2025 16:10:40 +0900 Subject: [PATCH 207/299] =?UTF-8?q?[REFACTOR/#294]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=20=EC=8B=9C=EA=B0=84=2021=EC=8B=9C=2030?= =?UTF-8?q?=EB=B6=84=EC=9D=84=209=EC=8B=9C=2030=EB=B6=84=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/auth/timereminder/TimeReminderScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt index e9f9f399..c9c3186b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt @@ -102,7 +102,7 @@ fun TimeReminderRoute( TimeReminderScreen( onStartClick = { - viewModel.setFixedTime(TimePeriod.PM, "21", "30") + viewModel.setFixedTime(TimePeriod.PM, "9", "30") viewModel.sendNotification(context, isNotificationPermissionGranted.value) }, onTimeSelected = { period, hour, minute -> From e256a020acfea6aed69684b6dbef70dcb5bfbfa8 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Thu, 10 Jul 2025 16:10:58 +0900 Subject: [PATCH 208/299] =?UTF-8?q?[REFACTOR/#294]=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=BC=80=EB=B0=A5=20=EB=B2=84=ED=8A=BC=EC=9D=B4=20=EC=9D=BC?= =?UTF-8?q?=EA=B8=B0=EA=B0=80=20=EC=9E=88=EC=9D=84=20=EB=95=8C=EB=A7=8C=20?= =?UTF-8?q?=EB=B3=B4=EC=9D=B4=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 --- .../calendar/component/DailyDiaryListItem.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt index 21192e99..d2bf1dc2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt @@ -62,13 +62,15 @@ fun DailyDiaryListItem( modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), ) Spacer(modifier = Modifier.weight(1f)) - Image( - painter = painterResource(id = R.drawable.ic_home_kebab), - contentDescription = "go to delete", - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .clickable(onClick = { onShowDiaryDeleteStateChange(true) }), - ) + if (dailyDiary.diaries.isNotEmpty()) { + Image( + painter = painterResource(id = R.drawable.ic_home_kebab), + contentDescription = "go to delete", + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .clickable(onClick = { onShowDiaryDeleteStateChange(true) }), + ) + } } when { From c1389fc9314a0d93973d683c9abf8ca461cc7b1d Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Thu, 10 Jul 2025 16:12:02 +0900 Subject: [PATCH 209/299] =?UTF-8?q?[REFACTOR/#294]=20LazyColumn=20?= =?UTF-8?q?=ED=8C=A8=EB=94=A9=EC=9D=84=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=B2=98=EB=A6=AC=EC=97=90=20=EB=8D=94=20=EC=A0=81?= =?UTF-8?q?=ED=95=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/writediary/screen/WriteDiaryScreen.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index 7ce2e175..5507e409 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource 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 @@ -14,10 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.union -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Scaffold @@ -252,10 +250,12 @@ fun WriteDiaryScreen( Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) LazyColumn( modifier = Modifier - .fillMaxSize() - .windowInsetsPadding( - WindowInsets.navigationBars.union(WindowInsets.ime), - ), + .fillMaxSize(), + contentPadding = PaddingValues( + bottom = with(density) { + (imeBottom - innerPadding.calculateBottomPadding().toPx()).coerceAtLeast(0f).toDp() + }, + ), verticalArrangement = Arrangement.spacedBy(12.dp), ) { itemsIndexed(entries, key = { index, _ -> index }) { index, text -> From 43159c84652d90a2a5ab395a4d61d2ae75300e49 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 11 Jul 2025 11:44:45 +0900 Subject: [PATCH 210/299] =?UTF-8?q?[FEAT/#293]=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=ED=99=94=EB=A9=B4=20=ED=98=84=EC=A7=80?= =?UTF-8?q?=ED=99=94=EB=A5=BC=20=EC=A7=84=ED=96=89=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20-=20=EC=9D=BC=EB=B6=80=20=EB=A6=AC=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?-=20=ED=99=94=EB=A9=B4=20=EB=82=B4=20=EB=A6=AC=EC=86=8C?= =?UTF-8?q?=EC=8A=A4,=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=93=B1=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=82=B4=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EB=B6=84=EB=A6=AC=20-=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/bottomsheet/DiaryDeleteSheet.kt | 2 +- .../DeleteWriteDiaryBottomSheet.kt | 2 +- .../writediary/component/button/SendButton.kt | 2 +- .../ui/writediary/screen/WriteDiaryScreen.kt | 20 +++---- app/src/main/res/values-en/strings.xml | 22 ++++++++ app/src/main/res/values/strings.xml | 55 +++++++++---------- 6 files changed, 62 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/bottomsheet/DiaryDeleteSheet.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/bottomsheet/DiaryDeleteSheet.kt index feecc8e6..4e3cb841 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/bottomsheet/DiaryDeleteSheet.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/bottomsheet/DiaryDeleteSheet.kt @@ -72,7 +72,7 @@ fun DiaryDeleteBottomSheetItem( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = stringResource(R.string.bottom_sheet_delete_button), + text = stringResource(R.string.bottom_sheet_diary_delete), style = ClodyTheme.typography.body4SemiBold, color = ClodyTheme.colors.gray01, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt index 890be429..30e0c758 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/bottomsheet/DeleteWriteDiaryBottomSheet.kt @@ -61,7 +61,7 @@ fun DeleteWriteDiaryBottomSheet( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = stringResource(R.string.bottom_sheet_delete_button), + text = stringResource(R.string.bottom_sheet_diary_delete), style = ClodyTheme.typography.body4SemiBold, color = ClodyTheme.colors.gray01, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt index 059e94e6..603ad84b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt @@ -35,7 +35,7 @@ fun SendButton( contentAlignment = Alignment.Center, ) { Text( - text = stringResource(R.string.write_diary_text_button), + text = stringResource(R.string.write_diary_send), color = if (isPressed) ClodyTheme.colors.gray07 else ClodyTheme.colors.gray01, style = ClodyTheme.typography.body2SemiBold, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index 7ce2e175..ffe3d65d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -281,10 +281,10 @@ fun WriteDiaryScreen( if (showDialog) { ClodyDialog( onDismiss = onDismissDialog, - titleMassage = stringResource(R.string.write_diary_dialog_title), - descriptionMassage = stringResource(R.string.write_diary_dialog_description), - confirmOption = stringResource(R.string.write_diary_dialog_confirm_option), - dismissOption = stringResource(R.string.write_diary_dialog_dismiss_option), + titleMassage = stringResource(R.string.dialog_write_diary_title), + descriptionMassage = stringResource(R.string.dialog_write_diary_description), + confirmOption = stringResource(R.string.dialog_write_diary_confirm), + dismissOption = stringResource(R.string.dialog_write_diary_dismiss), confirmAction = onConfirmDialog, confirmButtonColor = ClodyTheme.colors.mainYellow, confirmButtonTextColor = ClodyTheme.colors.gray01, @@ -300,10 +300,10 @@ fun WriteDiaryScreen( if (showExitDialog) { ClodyDialog( - titleMassage = stringResource(R.string.temp_save_dialog_exit_title), - descriptionMassage = stringResource(R.string.temp_save_dialog_exit_description), - confirmOption = stringResource(R.string.temp_save_dialog_exit_confirm), - dismissOption = stringResource(R.string.temp_save_dialog_exit_dismiss), + titleMassage = stringResource(R.string.dialog_draft_save_title), + descriptionMassage = stringResource(R.string.dialog_draft_save_description), + confirmOption = stringResource(R.string.dialog_draft_save_confirm), + dismissOption = stringResource(R.string.dialog_draft_save_dismiss), confirmAction = onConfirmExitDialog, confirmButtonColor = ClodyTheme.colors.red, confirmButtonTextColor = ClodyTheme.colors.white, @@ -356,7 +356,7 @@ private fun ShowToastMessages( ) { if (showLimitMessage) { ClodyToastMessage( - message = stringResource(R.string.toast_limit_message), + message = stringResource(R.string.toast_write_diary_entry_limit), iconResId = R.drawable.ic_toast_error, backgroundColor = ClodyTheme.colors.gray04, contentColor = ClodyTheme.colors.white, @@ -367,7 +367,7 @@ private fun ShowToastMessages( if (showEmptyFieldsMessage) { ClodyToastMessage( - message = stringResource(R.string.toast_empty_fields_message), + message = stringResource(R.string.toast_write_diary_entry_empty), iconResId = R.drawable.ic_toast_error, backgroundColor = ClodyTheme.colors.gray04, contentColor = ClodyTheme.colors.white, diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index b203e0ef..0dc2d685 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -63,6 +63,13 @@ Continue Cancel + Send + %1$d %2$d + Please avoid using slang, profanity, or emojis. + Something you\'re grateful for today. + Please enter between 2 and 100 characters. + Add + Settings Profile and Account Notification @@ -106,4 +113,19 @@ You won\'t receive another reply\nif you delete and rewrite your entry. Delete Cancel + + Ready to share\nyour journal with Lody? + Sent journals can\'t be edited. + Send + Cancel + + Want to send this later instead? + Your journal will be lost if you exit now. + Exit + Draft + + Field required to send. + You can write up to 5 entries. + + Delete diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63c83c77..f2745cec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,6 +80,14 @@ 이어쓰기 아니오 + + 보내기 + %1$d월 %2$d일 + 신조어, 비속어, 이모지 작성은 불가능해요 + 일상 속 작은 감사함을 적어보세요 + 2~50자 까지 입력할 수 있어요. + 추가하기 + 로디가 열심히 답장을 쓰고 있어요 로디가 쓴 행운의 답장이 도착했어요! @@ -97,33 +105,6 @@ 1개의 네잎클로버 획득 확인 - - 일기를 로디에게 보낼까요? - 보낸 일기는 수정이 어려워요. - 보내기 - 취소 - - - 지금까지 쓴 일기를 임시저장할까요? - 나가기를 누르면 작성 중인 내용이 모두 사라져요. - 나가기 - 임시저장 - - - 저장 - %1$d월 %2$d일 - 추가하기 - 보내기 - 일상 속 작은 감사함을 적어보세요 - 2~50자 까지 입력할 수 있어요. - - - 최대 5개까지 작성할 수 있어요. - 빈 칸을 채워야 보낼 수 있어요. - - - 신조어, 비속어, 이모지 작성은 불가능해요 - %1$s년 %2$s월 작성된 감사일기가 없어요 @@ -174,7 +155,6 @@ 알람 시간 설정을 완료했어요. - 삭제하기 다른 날짜 보기 완료 다시 시도 @@ -197,6 +177,25 @@ 삭제할래요 아니요 + + 일기를 로디에게 보낼까요? + 보낸 일기는 수정이 어려워요. + 보내기 + 취소 + + + 지금까지 쓴 일기를 임시저장할까요? + 나가기를 누르면 작성 중인 내용이 모두 사라져요. + 나가기 + 임시저장 + + + 최대 5개까지 작성할 수 있어요. + 빈 칸을 채워야 보낼 수 있어요. + + + 삭제하기 + 데이터를 불러오는데 실패했습니다. 알 수 없는 오류가 발생했습니다. From c8d3f83d3cbff7785af2b4595233cfa60e8dd2f3 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 11 Jul 2025 13:34:17 +0900 Subject: [PATCH 211/299] =?UTF-8?q?[FEAT/#293]=20=EB=8B=B5=EC=9E=A5?= =?UTF-8?q?=EB=8C=80=EA=B8=B0=20=ED=99=94=EB=A9=B4,=20=EB=8B=B5=EC=9E=A5?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=ED=98=84=EC=A7=80?= =?UTF-8?q?=ED=99=94=EB=A5=BC=20=EC=A7=84=ED=96=89=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20-=20=EC=9D=BC=EB=B6=80=20=EB=A6=AC=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?-=20=ED=99=94=EB=A9=B4=20=EB=82=B4=20=EB=A6=AC=EC=86=8C?= =?UTF-8?q?=EC=8A=A4,=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=93=B1=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=82=B4=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EB=B6=84=EB=A6=AC=20-=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/replydiary/ReplyDiaryScreen.kt | 8 +++---- .../component/QuickReplyAdButton.kt | 2 +- .../replyloading/screen/ReplyLoadingScreen.kt | 8 +++---- app/src/main/res/values-en/strings.xml | 13 +++++++++++ app/src/main/res/values/strings.xml | 23 +++++++++---------- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt index f112fc19..65efb8b1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt @@ -152,7 +152,7 @@ fun ReplyDiaryScreen( ) { val content = replyDiaryState.content val nickname = replyDiaryState.nickname - val replyMessage = stringResource(R.string.reply_message, nickname) + val replyMessage = stringResource(R.string.reply_title, nickname) Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) Image( @@ -187,9 +187,9 @@ fun ReplyDiaryScreen( if (showDialog) { CloverDialog( onDismiss = { showDialog = false }, - titleMassage = stringResource(R.string.clover_dialog_title, replyDiaryState.nickname), - descriptionMassage = stringResource(R.string.clover_dialog_description), - confirmOption = stringResource(R.string.clover_dialog_confirm_option), + titleMassage = stringResource(R.string.dialog_reply_clover_title, replyDiaryState.nickname), + descriptionMassage = stringResource(R.string.dialog_reply_clover_description), + confirmOption = stringResource(R.string.dialog_reply_clover_confirm), confirmAction = { showDialog = false }, confirmButtonColor = ClodyTheme.colors.mainYellow, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/component/QuickReplyAdButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/component/QuickReplyAdButton.kt index 49ddc451..4db6005c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/component/QuickReplyAdButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/component/QuickReplyAdButton.kt @@ -41,7 +41,7 @@ fun QuickReplyAdButton( tint = Color.Unspecified, ) Text( - text = stringResource(R.string.loading_button_watch_ad_and_get_reply), + text = stringResource(R.string.reply_loading_btn_ad_request), style = ClodyTheme.typography.body4Medium, color = ClodyTheme.colors.blue, modifier = Modifier.padding(start = 5.dp), diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt index cf221aad..5b931081 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt @@ -179,9 +179,9 @@ fun ReplyLoadingScreen( val minutes = ((remainingTime % 3600) / 60).toInt() val seconds = (remainingTime % 60).toInt() - val loadingMessage = stringResource(id = R.string.loading_message) - val nearlyDoneMessage = stringResource(id = R.string.loading_nearly_done_message) - val completeMessage = stringResource(id = R.string.loading_complete_message) + val loadingMessage = stringResource(id = R.string.reply_loading_initial_description) + val nearlyDoneMessage = stringResource(id = R.string.reply_loading_after_ad_description) + val completeMessage = stringResource(id = R.string.reply_loading_complete_description) // 메시지 분기 val textToShow = when { @@ -217,7 +217,7 @@ fun ReplyLoadingScreen( if (isComplete) AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.WAITING_DIARY_REPLY) onCompleteClick() }, - text = stringResource(R.string.loading_button_open), + text = stringResource(R.string.reply_loading_btn_open), enabled = isComplete, ) }, diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 0dc2d685..acbb1de2 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -70,6 +70,15 @@ Please enter between 2 and 100 characters. Add + Lody\'s working hard on your reply! + You\'ve got a lucky reply from Lody! + Lody\'s almost done writing your reply\\njust a little longer! + Watch Ad for Instant Reply + Open Reply + + %1$d %2$d + A lucky reply for %1$d + Settings Profile and Account Notification @@ -124,6 +133,10 @@ Exit Draft + %1$s, your luck is here! + 1 Four-Leaf Clover + Done + Field required to send. You can write up to 5 entries. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f2745cec..26776ec4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -89,21 +89,15 @@ 추가하기 - 로디가 열심히 답장을 쓰고 있어요 - 로디가 쓴 행운의 답장이 도착했어요! - 로디가 답장을 거의 다 써가요!\n조금만 기다려주세요 - 열어보기 - 확인 - 광고 보고 바로 답장 받기 + 로디가 열심히 답장을 쓰고 있어요 + 로디가 답장을 거의 다 써가요!\n조금만 기다려주세요 + 로디가 쓴 행운의 답장이 도착했어요! + 광고 보고 바로 답장 받기 + 열어보기 %1$d월 %2$d일 - %1$s님을 위한 행운의 답장 - - - %1$s님을 위한 행운 도착 - 1개의 네잎클로버 획득 - 확인 + %1$s님을 위한 행운의 답장 %1$s년 %2$s월 @@ -189,6 +183,11 @@ 나가기 임시저장 + + %1$s님을 위한 행운 도착 + 1개의 네잎클로버 획득 + 확인 + 최대 5개까지 작성할 수 있어요. 빈 칸을 채워야 보낼 수 있어요. From 44ed476f78af88675995c55a81764d5ef48b4dcb Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 11 Jul 2025 15:54:35 +0900 Subject: [PATCH 212/299] =?UTF-8?q?[FEAT/#293]=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=AA=A8=EC=95=84=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=ED=98=84=EC=A7=80=ED=99=94?= =?UTF-8?q?=EB=A5=BC=20=EC=A7=84=ED=96=89=ED=95=A9=EB=8B=88=EB=8B=A4.=20-?= =?UTF-8?q?=20=EC=9D=BC=EB=B6=80=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD=20-=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=82=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4,=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EB=93=B1=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=82=B4=20=EB=A6=AC?= =?UTF-8?q?=EC=86=8C=EC=8A=A4=20=EB=B6=84=EB=A6=AC=20-=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/component/timepicker/YearMonthPicker.kt | 4 ++-- .../ui/diarylist/component/DailyDiaryCard.kt | 6 +++--- .../ui/diarylist/component/DiaryListTopAppBar.kt | 2 +- app/src/main/res/values-en/strings.xml | 9 +++++++++ app/src/main/res/values/strings.xml | 12 ++++++------ 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt index f04f68eb..a2b1ea9a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt @@ -69,7 +69,7 @@ fun YearMonthPicker( .padding(top = 16.dp, bottom = 30.dp), ) { Text( - text = stringResource(R.string.year_month_picker_title), + text = stringResource(R.string.bottom_sheet_year_month_picker_title), style = ClodyTheme.typography.body2SemiBold, color = ClodyTheme.colors.gray01, modifier = Modifier.align(Alignment.Center), @@ -133,7 +133,7 @@ fun YearMonthPicker( onYearMonthSelected(year, month) onDismissRequest() }, - text = stringResource(R.string.year_month_picker_confirm), + text = stringResource(R.string.bottom_sheet_year_month_picker_confirm), enabled = true, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt index bd1d3fce..e596ced5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt @@ -85,14 +85,14 @@ fun DailyDiaryCard( verticalAlignment = Alignment.Bottom, ) { Text( - text = stringResource(R.string.diarylist_daily_diary_day, day), + text = stringResource(R.string.diary_list_daily_diary_day, day), modifier = Modifier .padding(end = 2.dp), color = ClodyTheme.colors.gray01, style = ClodyTheme.typography.body2SemiBold, ) Text( - text = stringResource(R.string.diarylist_daily_diary_day_of_week, dayOfWeek), + text = stringResource(R.string.diary_list_daily_diary_day_of_week, dayOfWeek), color = ClodyTheme.colors.gray04, style = ClodyTheme.typography.body4Medium, ) @@ -155,7 +155,7 @@ fun ReplyDiaryButton( contentPadding = PaddingValues(0.dp), ) { Text( - text = stringResource(R.string.diarylist_check_reply), + text = stringResource(R.string.diary_list_check_reply), modifier = Modifier .padding(horizontal = 10.dp, vertical = 2.dp), style = ClodyTheme.typography.detail1SemiBold, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DiaryListTopAppBar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DiaryListTopAppBar.kt index e6a80526..a2b0d8ae 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DiaryListTopAppBar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DiaryListTopAppBar.kt @@ -50,7 +50,7 @@ fun DiaryListTopAppBar( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = stringResource(R.string.diarylist_selected_year_month, selectedYear, selectedMonth), + text = stringResource(R.string.diary_list_selected_year_month, selectedYear, selectedMonth), color = ClodyTheme.colors.gray01, style = ClodyTheme.typography.head4, ) diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index acbb1de2..0bfc2b2f 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -79,6 +79,12 @@ %1$d %2$d A lucky reply for %1$d + %2$s %1$s + No gratitude entries yet. + %1$s + /%1$s + Reply + Settings Profile and Account Notification @@ -141,4 +147,7 @@ You can write up to 5 entries. Delete + + View Another Day + Done diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 26776ec4..c4b16ac4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,11 +100,11 @@ %1$s님을 위한 행운의 답장 - %1$s년 %2$s월 + %1$s년 %2$s월 작성된 감사일기가 없어요 - %1$s일 - /%1$s - 답장 확인 + %1$s일 + /%1$s + 답장 확인 설정 @@ -149,8 +149,8 @@ 알람 시간 설정을 완료했어요. - 다른 날짜 보기 - 완료 + 다른 날짜 보기 + 완료 다시 시도 확인 From b610910112cdfcd99b0d693770735693e9b48d09 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 12 Jul 2025 17:35:50 +0900 Subject: [PATCH 213/299] =?UTF-8?q?[FEAT/#293]=20=EC=9D=B4=EC=99=B8=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=EB=93=A4=EC=9D=84=20=EB=B2=88?= =?UTF-8?q?=EC=97=AD=ED=95=98=EA=B3=A0=20strings.xml=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=9D=84=20=EC=A0=95=EB=A6=AC=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timepicker/BottomSheetTimePicker.kt | 4 +- .../presentation/ui/auth/guide/GuideScreen.kt | 4 +- .../ui/auth/signup/page/NicknamePage.kt | 2 +- .../ui/auth/signup/page/TermsOfServicePage.kt | 2 +- .../auth/timereminder/TimeReminderScreen.kt | 4 +- .../ui/component/FailureScreen.kt | 2 +- .../ui/component/dialog/FailureDialog.kt | 2 +- .../component/timepicker/YearMonthPicker.kt | 2 +- .../ui/diarylist/component/DailyDiaryCard.kt | 2 +- .../ui/home/component/DiaryStateButton.kt | 10 +- .../presentation/ui/home/screen/HomeScreen.kt | 20 +- .../AccountManagementLogoutOption.kt | 2 +- .../AccountManagementNicknameOption.kt | 2 +- .../AccountManagementRevokeOption.kt | 2 +- .../component/NicknameChangeBottomSheet.kt | 4 +- .../NotificationSettingTimePicker.kt | 4 +- .../screen/NotificationSettingScreen.kt | 2 +- .../setting/screen/AccountManagementScreen.kt | 18 +- .../presentation/ui/splash/SplashScreen.kt | 16 +- .../component/button/AddDiaryEntryFAB.kt | 2 +- .../writediary/component/button/SendButton.kt | 2 +- app/src/main/res/values-en/strings.xml | 147 +++++++++----- app/src/main/res/values/strings.xml | 179 +++++++++--------- 23 files changed, 235 insertions(+), 199 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt index 175232d5..657ad047 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt @@ -73,7 +73,7 @@ fun BottomSheetTimePicker( .padding(top = 16.dp, bottom = 30.dp), ) { Text( - stringResource(id = R.string.time_reminder_picker_title), + stringResource(id = R.string.bottom_sheet_time_reminder_picker_title), style = ClodyTheme.typography.head4, color = ClodyTheme.colors.gray01, modifier = Modifier.align(Alignment.Center), @@ -143,7 +143,7 @@ fun BottomSheetTimePicker( onRemindTimeSelected(selectedPeriod, selectedHour, selectedMinute) onDismissRequest() }, - text = stringResource(R.string.time_reminder_complete_button), + text = stringResource(R.string.bottom_sheet_year_month_picker_btn_confirm), enabled = true, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt index 75a736f1..02422827 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt @@ -100,9 +100,9 @@ fun GuideScreen( } }, text = if (pagerState.currentPage < pages.size - 1) { - stringResource(id = R.string.guide_next) + stringResource(id = R.string.guide_btn_next) } else { - stringResource(id = R.string.guide_start) + stringResource(id = R.string.guide_btn_start) }, enabled = true, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt index a716c61f..e1f2f9c0 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt @@ -86,7 +86,7 @@ fun NickNamePage( focusManager.clearFocus() onCompleteClick() }, - text = stringResource(R.string.nickname_next), + text = stringResource(R.string.nickname_btn_next), enabled = nickname.isNotEmpty() && isValidNickname, ) }, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt index 379cf831..df40e950 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt @@ -73,7 +73,7 @@ fun TermsOfServicePage( .padding(horizontal = 24.dp) .padding(bottom = 28.dp), onClick = onAgreeClick, - text = stringResource(R.string.terms_next), + text = stringResource(R.string.terms_btn_next), enabled = isAgreeButtonEnabled, ) }, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt index e9f9f399..f2e7523a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt @@ -147,13 +147,13 @@ fun TimeReminderScreen( ) { ClodyButton( onClick = onCompleteClick, - text = stringResource(id = R.string.time_reminder_complete_button), + text = stringResource(id = R.string.time_reminder_btn_complete), enabled = true, modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(LocalConfiguration.current.screenHeightDp.dp * 0.015f)) Text( - text = stringResource(id = R.string.time_reminder_next_setting_button), + text = stringResource(id = R.string.time_reminder_btn_skip), modifier = Modifier .clickable( onClick = onStartClick, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/FailureScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/FailureScreen.kt index adfee414..feb23e6a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/FailureScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/FailureScreen.kt @@ -59,7 +59,7 @@ fun FailureScreen( colors = ButtonDefaults.buttonColors(ClodyTheme.colors.mainYellow), ) { Text( - text = stringResource(R.string.failure_screen_refresh_btn), + text = stringResource(R.string.failure_screen_btn_refresh), modifier = Modifier.padding(6.dp), color = ClodyTheme.colors.gray01, style = ClodyTheme.typography.body2SemiBold, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/FailureDialog.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/FailureDialog.kt index 6ff6a936..6c13d329 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/FailureDialog.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/FailureDialog.kt @@ -83,7 +83,7 @@ fun FailureDialog( colors = ButtonDefaults.buttonColors(ClodyTheme.colors.mainYellow), ) { Text( - text = stringResource(R.string.failure_dialog_confirm_btn), + text = stringResource(R.string.failure_dialog_btn_confirm), color = ClodyTheme.colors.gray02, style = ClodyTheme.typography.body3SemiBold, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt index a2b1ea9a..181cc474 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt @@ -133,7 +133,7 @@ fun YearMonthPicker( onYearMonthSelected(year, month) onDismissRequest() }, - text = stringResource(R.string.bottom_sheet_year_month_picker_confirm), + text = stringResource(R.string.bottom_sheet_year_month_picker_btn_confirm), enabled = true, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt index e596ced5..ed6029f9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt @@ -155,7 +155,7 @@ fun ReplyDiaryButton( contentPadding = PaddingValues(0.dp), ) { Text( - text = stringResource(R.string.diary_list_check_reply), + text = stringResource(R.string.diary_list_btn_reply), modifier = Modifier .padding(horizontal = 10.dp, vertical = 2.dp), style = ClodyTheme.typography.detail1SemiBold, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt index 58b2453f..37431fde 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt @@ -31,7 +31,7 @@ fun DiaryStateButton( hasDraft -> { ClodyButton( onClick = { onClickWriteDiary(year, month, day) }, - text = stringResource(R.string.home_button_continue_draft), + text = stringResource(R.string.home_btn_continue_draft), enabled = true, modifier = modifier, ) @@ -40,7 +40,7 @@ fun DiaryStateButton( isInvalidDraft -> { ClodyButton( onClick = { /* no-action */ }, - text = stringResource(R.string.home_button_check_reply), + text = stringResource(R.string.home_btn_check_reply), enabled = false, modifier = modifier, disabledContainerColor = ClodyTheme.colors.gray05, @@ -51,7 +51,7 @@ fun DiaryStateButton( canReply -> { ClodyReplyButton( onClick = onClickReplyDiary, - text = stringResource(R.string.home_button_check_reply), + text = stringResource(R.string.home_btn_check_reply), enabled = true, modifier = modifier, ) @@ -60,7 +60,7 @@ fun DiaryStateButton( canWrite -> { ClodyButton( onClick = { onClickWriteDiary(year, month, day) }, - text = stringResource(R.string.home_button_write_diary), + text = stringResource(R.string.home_btn_write_diary), enabled = true, modifier = modifier, ) @@ -69,7 +69,7 @@ fun DiaryStateButton( else -> { ClodyButton( onClick = { onClickWriteDiary(year, month, day) }, - text = stringResource(R.string.home_button_write_diary), + text = stringResource(R.string.home_btn_write_diary), enabled = false, modifier = modifier, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index bd3ed68e..693a5fab 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -172,28 +172,28 @@ fun HomeRoute( .padding(horizontal = 16.dp), ) { Text( - text = stringResource(R.string.home_draft_popup_title), + text = stringResource(R.string.bottom_sheet_home_initial_draft_title), color = ClodyTheme.colors.gray01, textAlign = TextAlign.Center, style = ClodyTheme.typography.head3, ) Spacer(modifier = Modifier.height(10.dp)) Text( - text = stringResource(R.string.home_draft_popup_description), + text = stringResource(R.string.bottom_sheet_home_initial_draft_description), color = ClodyTheme.colors.gray04, textAlign = TextAlign.Center, style = ClodyTheme.typography.body3Regular, ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = stringResource(R.string.home_draft_popup_guide), + text = stringResource(R.string.bottom_sheet_home_initial_draft_guide), color = ClodyTheme.colors.gray04, textAlign = TextAlign.Center, style = ClodyTheme.typography.body3Regular, ) Spacer(modifier = Modifier.height(28.dp)) ClodyButton( - text = stringResource(R.string.home_draft_popup_accept), + text = stringResource(R.string.bottom_sheet_home_initial_draft_accept), onClick = { homeViewModel.enableDraftAlarm(context) homeViewModel.updateFirstDraftUse(false) @@ -202,7 +202,7 @@ fun HomeRoute( modifier = Modifier.fillMaxWidth(), ) Text( - text = stringResource(R.string.home_draft_popup_dismiss), + text = stringResource(R.string.bottom_sheet_home_initial_draft_skip), modifier = Modifier .clickable(onClick = { homeViewModel.updateFirstDraftUse(false) }) .padding(12.dp), @@ -221,7 +221,7 @@ fun HomeRoute( contentAlignment = Alignment.BottomCenter, content = { ClodyToastMessage( - message = stringResource(R.string.home_draft_alarm_enabled_toast), + message = stringResource(R.string.toast_home_draft_alarm_enabled), iconResId = R.drawable.ic_toast_check_on_18, backgroundColor = ClodyTheme.colors.gray04, contentColor = ClodyTheme.colors.white, @@ -237,10 +237,10 @@ fun HomeRoute( if (showContinueDraftDialog) { ClodyDialog( - titleMassage = stringResource(R.string.home_continue_draft_dialog_title), - descriptionMassage = stringResource(R.string.home_continue_draft_dialog_description), - confirmOption = stringResource(R.string.home_continue_draft_dialog_confirm), - dismissOption = stringResource(R.string.home_continue_draft_dialog_dismiss), + titleMassage = stringResource(R.string.dialog_home_continue_draft_title), + descriptionMassage = stringResource(R.string.dialog_home_continue_draft_description), + confirmOption = stringResource(R.string.dialog_home_continue_draft_confirm), + dismissOption = stringResource(R.string.dialog_home_continue_draft_dismiss), confirmAction = { homeViewModel.setShowContinueDraftDialog(false) val date = homeViewModel.selectedDate.value diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt index 61281c6e..927b01be 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt @@ -41,7 +41,7 @@ fun AccountManagementLogoutOption( ) Spacer(modifier = Modifier.weight(1f)) Text( - text = stringResource(R.string.account_management_logout_button), + text = stringResource(R.string.account_management_btn_logout), modifier = Modifier.clickable( onClick = { updateLogoutDialog(true) }, indication = null, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementNicknameOption.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementNicknameOption.kt index c1bd0e37..cbc43285 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementNicknameOption.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementNicknameOption.kt @@ -43,7 +43,7 @@ fun AccountManagementNicknameOption( ) Spacer(modifier = Modifier.weight(1f)) Text( - text = stringResource(R.string.account_management_nickname_change_button), + text = stringResource(R.string.account_management_btn_change_nickname), modifier = Modifier.clickable( onClick = { updateNicknameChangeBottomSheet(true) }, indication = null, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementRevokeOption.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementRevokeOption.kt index 95bad3d2..4f3a8e98 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementRevokeOption.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementRevokeOption.kt @@ -28,7 +28,7 @@ fun AccountManagementRevokeOption( ) Spacer(modifier = Modifier.weight(1f)) Text( - text = stringResource(R.string.account_management_revoke_button), + text = stringResource(R.string.account_management_btn_revoke), modifier = Modifier.clickable( onClick = { updateRevokeDialog(true) }, indication = null, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt index 1caf228f..7110b7db 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt @@ -84,7 +84,7 @@ fun NicknameChangeBottomSheetItem( .padding(top = 8.dp), ) { Text( - text = stringResource(R.string.account_management_nickname_change_title), + text = stringResource(R.string.bottom_sheet_nickname_change_title), modifier = Modifier.align(Alignment.Center), color = ClodyTheme.colors.gray01, style = ClodyTheme.typography.body2SemiBold, @@ -163,7 +163,7 @@ fun NicknameChangeBottomSheetItem( accountManagementViewModel.changeNickname(ModifyNicknameRequestDto(name = nickname.text)) onDismiss() }, - text = stringResource(R.string.account_management_nickname_change_confirm), + text = stringResource(R.string.bottom_sheet_nickname_change_confirm), enabled = nicknameChangeState && isValidNickname, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt index 801a019a..ccf2a97c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt @@ -66,7 +66,7 @@ fun NotificationSettingTimePicker( .padding(top = 16.dp, bottom = 30.dp), ) { Text( - text = stringResource(R.string.notification_setting_picker_title), + text = stringResource(R.string.bottom_sheet_notification_time_change_title), style = ClodyTheme.typography.head4, color = ClodyTheme.colors.gray01, modifier = Modifier.align(Alignment.Center), @@ -144,7 +144,7 @@ fun NotificationSettingTimePicker( ).to24HourFormat() onConfirm(selectedTime) }, - text = stringResource(R.string.notification_setting_picker_confirm), + text = stringResource(R.string.bottom_sheet_notification_time_change_confirm), enabled = true, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index 2fca35b5..f0410dde 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -85,7 +85,7 @@ fun NotificationSettingRoute( contentAlignment = Alignment.BottomCenter, ) { ClodyToastMessage( - message = stringResource(R.string.notification_setting_change_success_toast), + message = stringResource(R.string.toast_notification_setting_time_change), iconResId = R.drawable.ic_toast_check_on_18, backgroundColor = ClodyTheme.colors.gray04, contentColor = ClodyTheme.colors.white, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt index a7dba39a..c5be21b1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt @@ -174,7 +174,7 @@ fun AccountManagementScreen( contentAlignment = Alignment.BottomCenter, ) { ClodyToastMessage( - message = stringResource(R.string.account_management_nickname_change_toast), + message = stringResource(R.string.toast_account_management_nickname_change), iconResId = R.drawable.ic_toast_check_on_18, backgroundColor = ClodyTheme.colors.gray04, contentColor = ClodyTheme.colors.white, @@ -186,10 +186,10 @@ fun AccountManagementScreen( if (showLogoutDialog) { LogoutDialog( - titleMassage = stringResource(R.string.account_management_logout_dialog_title), - descriptionMassage = stringResource(R.string.account_management_logout_dialog_description), - confirmOption = stringResource(R.string.account_management_logout_dialog_confirm), - dismissOption = stringResource(R.string.account_management_logout_dialog_dismiss), + titleMassage = stringResource(R.string.dialog_logout_title), + descriptionMassage = stringResource(R.string.dialog_logout_description), + confirmOption = stringResource(R.string.dialog_logout_confirm), + dismissOption = stringResource(R.string.dialog_logout_dismiss), confirmAction = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.LOGOUT) accountManagementViewModel.logOutAccount() @@ -200,10 +200,10 @@ fun AccountManagementScreen( if (showRevokeDialog) { ClodyDialog( - titleMassage = stringResource(R.string.account_management_revoke_dialog_title), - descriptionMassage = stringResource(R.string.account_management_revoke_dialog_description), - confirmOption = stringResource(R.string.account_management_revoke_dialog_confirm), - dismissOption = stringResource(R.string.account_management_revoke_dialog_dismiss), + titleMassage = stringResource(R.string.dialog_revoke_title), + descriptionMassage = stringResource(R.string.dialog_revoke_description), + confirmOption = stringResource(R.string.dialog_revoke_confirm), + dismissOption = stringResource(R.string.dialog_revoke_dismiss), confirmAction = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.REVOKE) accountManagementViewModel.revokeAccount() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt index a14ff51f..fc57d17a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt @@ -121,21 +121,21 @@ fun SoftUpdateDialog( ) { AlertDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.soft_update_title)) }, + title = { Text(stringResource(R.string.dialog_soft_update_title)) }, text = { Text( - text = stringResource(R.string.soft_update_message, latestVersion), + text = stringResource(R.string.dialog_soft_update_description, latestVersion), textAlign = TextAlign.Center, ) }, confirmButton = { TextButton(onClick = onConfirm) { - Text(stringResource(R.string.soft_update_confirm)) + Text(stringResource(R.string.dialog_soft_update_confirm)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text(stringResource(R.string.soft_update_dismiss)) + Text(stringResource(R.string.dialog_soft_update_dismiss)) } }, ) @@ -149,18 +149,18 @@ fun HardUpdateDialog( ) { AlertDialog( onDismissRequest = {}, - title = { Text(stringResource(R.string.hard_update_title)) }, + title = { Text(stringResource(R.string.dialog_hard_update_title)) }, text = { - Text(stringResource(R.string.hard_update_message, latestVersion)) + Text(stringResource(R.string.dialog_hard_update_description, latestVersion)) }, confirmButton = { TextButton(onClick = onConfirm) { - Text(stringResource(R.string.soft_update_confirm)) + Text(stringResource(R.string.dialog_soft_update_confirm)) } }, dismissButton = { TextButton(onClick = onExit) { - Text(stringResource(R.string.hard_update_exit)) + Text(stringResource(R.string.dialog_hard_update_exit)) } }, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt index 9f5b16ec..9331489c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/AddDiaryEntryFAB.kt @@ -86,7 +86,7 @@ fun BoxScope.AddDiaryEntryFAB( ) Spacer(modifier = Modifier.width(10.dp)) Text( - text = stringResource(R.string.write_diary_add_entry_fab), + text = stringResource(R.string.write_diary_fab_add_entry), color = contentColor, style = ClodyTheme.typography.body2SemiBold, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt index 603ad84b..062120c1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/button/SendButton.kt @@ -35,7 +35,7 @@ fun SendButton( contentAlignment = Alignment.Center, ) { Text( - text = stringResource(R.string.write_diary_send), + text = stringResource(R.string.write_diary_btn_send), color = if (isPressed) ClodyTheme.colors.gray07 else ClodyTheme.colors.gray01, style = ClodyTheme.typography.body2SemiBold, ) diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 0bfc2b2f..3cfef1ae 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -2,32 +2,29 @@ Clody - Update Available - A new version %1$s is available. Do you want to update now? - Update - Later - Update Required - Please update to version %1$s to continue - Exit App - + Sign Up With KaKao Sign Up With Google + To use Clody,\nPlease agree to the terms\nand conditions Agree to all (Required) Terms of Service (Required) Privacy Policy - Next + Next + Nice to meet you!\nWhat should I call you? Please enter your nickname - Next + Next + What time would you\nlike us to remind you to write? - Change reminder time - Save - Skip for now + %2$s:%3$s %1$s + Save + Skip for now + Hello!\nI\'m Lody I read your journal entries\nand reply with compliments\nand words of encouragement! @@ -40,51 +37,49 @@ Ready to write\nyour first journal?\nI\'ll be waiting! From your second journal onward,\nyour next clover will take 12 hours to grow - Next - Get Started + Next + Get Started + %1$d Clovers %2$d %1$d + No gratitude entries yet. There are existing entries in drafts. %1$d. %2$s %1$s - Write a Journal - See my Reply - Continue Writing - Heads up- drafts expire\\nDon\'t miss Lody\'s reply! - To receive a reminder before the reply deadline,\nconfirm notification permissions. - [Settings > Apps > Clody > Notifications] - Turn on Notifications - Skip for now - Continue writing reminders are now on! - Pick up where you left off? - This journal\'s reply time has expired. - Continue - Cancel - - Send + + Write a Journal + See my Reply + Continue Writing + + + Send %1$d %2$d Please avoid using slang, profanity, or emojis. Something you\'re grateful for today. Please enter between 2 and 100 characters. - Add + Add + Lody\'s working hard on your reply! You\'ve got a lucky reply from Lody! Lody\'s almost done writing your reply\\njust a little longer! Watch Ad for Instant Reply Open Reply + %1$d %2$d A lucky reply for %1$d + %2$s %1$s No gratitude entries yet. %1$s /%1$s - Reply + Reply + Settings Profile and Account Notification @@ -95,34 +90,40 @@ Version Fail to Fetch + Profile and Account " " - Edit - Please write without spaces or special characters - Save - Save Changes - Logout - Cancel + Edit + Logout Are you sure you want to delete your account? - Withdraw - Log out now? - I\'ll be here, see you again soon! - Logout - Delete your account? - Your journals, replies, and clovers will be\\npermanently deleted and can\'t be restored. - Withdraw - Cancel - Edit Nickname + Withdraw + Notification Journal Reminder Draft Reminder Reminder Time - Reply Notification - Choose another time - Save %2$s:%3$s %1$s - Save your reminder time + Reply Notification + + + Try Again + Close + + + Update Available + A new version %1$s is available. Do you want to update now? + Update + Later + + Update Required + Please update to version %1$s to continue + Exit App + + Pick up where you left off? + This journal\'s reply time has expired. + Continue + Cancel Delete this entry? You won\'t receive another reply\nif you delete and rewrite your entry. @@ -143,11 +144,53 @@ 1 Four-Leaf Clover Done + Log out now? + I\'ll be here, see you again soon! + Logout + Cancel + + Delete your account? + Your journals, replies, and clovers will be\\npermanently deleted and can\'t be restored. + Withdraw + Cancel + + + Continue writing reminders are now on! Field required to send. You can write up to 5 entries. + Save Changes + Save your reminder time + + + Change reminder time + Save + + Heads up- drafts expire\\nDon\'t miss Lody\'s reply! + To receive a reminder before the reply deadline,\nconfirm notification permissions. + [Settings > Apps > Clody > Notifications] + Turn on Notifications + Skip for now Delete View Another Day - Done + Done + + Edit Nickname + Please write without spaces or special characters + Save + + Choose another time + Save + + + AM + PM + %1$d + %1$d + An error occurred while deleting the diary. + Chrome must be installed to log in. + Login is not available on rooted devices for security reasons. + Failed to load data. + An unexpected error has occurred. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4b16ac4..a70be7ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,42 +1,29 @@ 클로디 - - - 업데이트 필요 - 새로운 버전 %1$s을 사용할 수 있습니다.\n지금 업데이트하시겠습니까? - 업데이트 - 나중에 - - - 필수 업데이트 - 버전 %1$s으로 업데이트가 필요합니다. - 앱 종료 - - + 카카오로 로그인 Sign Up With Google - - + "Clody 이용을 위해\n약관에 동의해 주세요" 전체 동의하기 (필수) 서비스 이용약관 (필수) 개인정보 처리방침 - 다음 + 다음 - + 만나서 반가워요!\n어떻게 불러 드릴까요? 닉네임을 입력해주세요 - 다음 + 다음 - + 몇시에 감사일기\n작성 알림을 드릴까요? - 다음에 설정할게요 - 발송 시간 변경 - 완료 + %1$s %2$s시 %3$s분 + 완료 + 다음에 설정할게요 - + 안녕하세요!\n저는 로디라고 해요 여러분이 써준 감사일기를 받고,\n칭찬과 응원을 담아 답장을 쓴답니다 @@ -49,10 +36,10 @@ 이제 일기를 써볼까요?\n기다리고 있을게요! 두번째 일기부터는 네잎클로버를 찾는 데\n12시간이 걸리니 조금만 기다려 주세요 - 다음 - 시작하기 + 다음 + 시작하기 - + 클로버 %1$d개 %1$d년 %2$d월 @@ -61,52 +48,37 @@ %1$d. %2$s %1$s요일 - - 일기 쓰기 - 답장 확인 - 이어 쓰기 - - - 기한이 지나면\n로디의 답장을 받을 수 없어요! - 답장 마감 전에 일기를 이어쓸 수 있도록\n알려드리기 위해서는 알림 설정이 필요해요. - [설정 > 애플리케이션 > 클로디 > 알림 > 알림표시] - 알림 받기 - 다음에 하기 - 이어쓰기 알림 설정을 완료했어요. - - - 임시저장된 일기를 이어 쓸까요? - 답장 기한이 지나서 답장은 받을 수 없어요. - 이어쓰기 - 아니오 - - - 보내기 + 일기 쓰기 + 답장 확인 + 이어 쓰기 + + + 보내기 %1$d월 %2$d일 신조어, 비속어, 이모지 작성은 불가능해요 일상 속 작은 감사함을 적어보세요 2~50자 까지 입력할 수 있어요. - 추가하기 + 추가하기 - + 로디가 열심히 답장을 쓰고 있어요 로디가 답장을 거의 다 써가요!\n조금만 기다려주세요 로디가 쓴 행운의 답장이 도착했어요! 광고 보고 바로 답장 받기 열어보기 - + %1$d월 %2$d일 %1$s님을 위한 행운의 답장 - + %1$s년 %2$s월 작성된 감사일기가 없어요 %1$s일 /%1$s - 답장 확인 + 답장 확인 - + 설정 프로필 및 계정 관리 알림 설정 @@ -117,86 +89,107 @@ 앱 버전 버전 정보를 불러오는데 실패했습니다. - + 프로필 및 계정 관리 - 변경하기 - 닉네임 변경 - 특수문자, 띄어쓰기 없이 작성해주세요 - 변경하기 - 변경을 완료했어요. - 로그아웃 - 로그아웃 하시겠어요? - 기다릴게요, 다음에 다시 만나요! - 로그아웃 - 아니요 + 변경하기 + 로그아웃 계정을 삭제하시겠어요? - 회원탈퇴 - 서비스를 탈퇴하시겠어요? - 작성하신 일기와 받은 답장 및 클로버가\n 모두 삭제되며 복구할 수 없어요. - 탈퇴할래요 - 아니요 + 회원탈퇴 - + 알림 설정 일기 작성 알림 받기 이어쓰기 알림 받기 알림 시간 - 답장 도착 알림 받기 - 다른 시간 보기 - 완료 %1$s %2$s시 %3$s분 - 알람 시간 설정을 완료했어요. + 답장 도착 알림 받기 - - 다른 날짜 보기 - 완료 - 다시 시도 - 확인 + + 다시 시도 + 확인 - - 오전 - 오후 - %1$s %2$s시 %3$s분 + + 업데이트 필요 + 새로운 버전 %1$s을 사용할 수 있습니다.\n지금 업데이트하시겠습니까? + 업데이트 + 나중에 - %1$d년 - %1$d월 + 필수 업데이트 + 버전 %1$s으로 업데이트가 필요합니다. + 앱 종료 - Chrome이 설치되어 있어야 로그인이 가능합니다. - 루팅된 기기에서는 로그인할 수 없습니다. + 임시저장된 일기를 이어 쓸까요? + 답장 기한이 지나서 답장은 받을 수 없어요. + 이어쓰기 + 아니오 - 정말 일기를 삭제할까요? 아직 답장이 오지 않았거나 삭제하고\n다시 작성한 일기는 답장을 받을 수 없어요. 삭제할래요 아니요 - 일기를 로디에게 보낼까요? 보낸 일기는 수정이 어려워요. 보내기 취소 - 지금까지 쓴 일기를 임시저장할까요? 나가기를 누르면 작성 중인 내용이 모두 사라져요. 나가기 임시저장 - %1$s님을 위한 행운 도착 1개의 네잎클로버 획득 확인 - + 로그아웃 하시겠어요? + 기다릴게요, 다음에 다시 만나요! + 로그아웃 + 아니요 + + 서비스를 탈퇴하시겠어요? + 작성하신 일기와 받은 답장 및 클로버가\n 모두 삭제되며 복구할 수 없어요. + 탈퇴할래요 + 아니요 + + + 이어쓰기 알림 설정을 완료했어요. 최대 5개까지 작성할 수 있어요. 빈 칸을 채워야 보낼 수 있어요. + 변경을 완료했어요. + 알람 시간 설정을 완료했어요. + 발송 시간 변경 + 완료 + + 기한이 지나면\n로디의 답장을 받을 수 없어요! + 답장 마감 전에 일기를 이어쓸 수 있도록\n알려드리기 위해서는 알림 설정이 필요해요. + [설정 > 애플리케이션 > 클로디 > 알림 > 알림표시] + 알림 받기 + 다음에 하기 + 삭제하기 - + 다른 날짜 보기 + 완료 + + 닉네임 변경 + 특수문자, 띄어쓰기 없이 작성해주세요 + 변경하기 + + 다른 시간 보기 + 완료 + + + 오전 + 오후 + %1$d년 + %1$d월 데이터를 불러오는데 실패했습니다. 알 수 없는 오류가 발생했습니다. 일기 삭제 중 오류가 발생했습니다. + Chrome이 설치되어 있어야 로그인이 가능합니다. + 루팅된 기기에서는 로그인할 수 없습니다. From 1640a59fbe1eaf73ad8951f3a28cabb39dab0c70 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 12 Jul 2025 17:52:53 +0900 Subject: [PATCH 214/299] =?UTF-8?q?[REFACTOR/#293]=20=EC=98=81=EC=96=B4=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=EB=A5=BC=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=84=A4=EC=A0=95=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20=EC=9D=B4=EC=A0=9C=20=ED=95=9C=EA=B5=AD=EC=96=B4?= =?UTF-8?q?=20=EC=9D=B4=EC=99=B8=20=EC=96=B8=EC=96=B4=EB=93=A4=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EB=90=98=EC=96=B4=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EA=B8=B0=EB=93=A4=EC=9D=80=20=EC=95=B1=EC=9D=84=20?= =?UTF-8?q?=EC=98=81=EC=96=B4=20=EB=B2=84=EC=A0=84=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=B4=EA=B2=8C=20=EB=90=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/res/drawable-en/img_splash_logo.png | Bin 17236 -> 0 bytes .../img_splash_logo.png | Bin .../res/drawable-xhdpi/img_splash_logo.png | Bin 11822 -> 0 bytes app/src/main/res/drawable/img_splash_logo.png | Bin 6135 -> 14774 bytes app/src/main/res/values-en/strings.xml | 196 ---------- app/src/main/res/values-ko/strings.xml | 196 ++++++++++ app/src/main/res/values/strings.xml | 359 +++++++++--------- 7 files changed, 376 insertions(+), 375 deletions(-) delete mode 100644 app/src/main/res/drawable-en/img_splash_logo.png rename app/src/main/res/{drawable-xxhdpi => drawable-ko}/img_splash_logo.png (100%) delete mode 100644 app/src/main/res/drawable-xhdpi/img_splash_logo.png delete mode 100644 app/src/main/res/values-en/strings.xml create mode 100644 app/src/main/res/values-ko/strings.xml diff --git a/app/src/main/res/drawable-en/img_splash_logo.png b/app/src/main/res/drawable-en/img_splash_logo.png deleted file mode 100644 index 81b70d4799e82b1f3a41cec2fecc91886590c71a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17236 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

%7;MjWDOcm^Lywp7P$)nV5!or&-Zv-6ofv zQ%A#xcl1bX?#l9}Ze|+lEYms^0Wrsy+5Q{4E{2~-JzbOgs}12Fy2W*NtjrgB?jO?6 z9v72p+7{OpHO$`5d`H*#b4yiXBd6`{$yU>|qri|0dg*9SJhoC@AvY8Z!zqkK*b#fp z4Z!e3LtpdW5ea6}wFRPUV`=H1W}T*oUvD^I^QebZSa$?Bwc~BFC(QvJqBT8fz4O@wH4b+cs?FnMqPp zGlw?A5}pAM(R9YiDb9pRFn^ll<6+q*fn%BtEW@=h%)o(n^@`^>E~p;y4G)YNUbe8u z1dV5H=$+Xm~u4tR6?xWlV3zr2Oz zJyJ6>W(o>*2{BV#l9PU<+CPG2`%^QQ)9ET$A1r5&1Y`fl(UNUmWdHxj2TMDSZo)H# z9gzQoi}^X!={XglbWJ||uQyJ9P#4wZ!fq$^3F|-7h}59UR$89?&42HrSSF1n`Bi!s zq!P*gN4qnn&;^x;6Nt(FA7QaTBA_}>m$j6${CEGo@n_1;K3=Q%*T3tJ^u!nzEI|{X z`2p4mYRmf#-v5L%wTr&!hv7kVSV{;G&UT}AkUC(N6E-lrWBS}5;b0&QcH@p>bis#7 z#>4aB)Bgy50m}!nV}TtJ@Bh&yu#Ow(RKl>*zabXY%bf2pgxwPV@fWI3JL)Qa`o&1~ zuc^~k;##jOO&YpTZH!3=3+D!a;SH3)4hNuxgnv0HK#aLiceq2Ar~e~^B+D}~NayK) z25&UHCF#g0?EjXAREiY}42CqJuK&97l@FX2HLCPIE@TajisJTub%FZ+%Y9XzFO80X z<-V?TDKQHDlOlWd_0IVAhmKlMIoNHymLVBzSDGqxT=~By&;y)+=6d{}-EQ`)mrf@| SCjk$^VDNPHb6Mw<&;$VC5^^a3 diff --git a/app/src/main/res/drawable-xxhdpi/img_splash_logo.png b/app/src/main/res/drawable-ko/img_splash_logo.png similarity index 100% rename from app/src/main/res/drawable-xxhdpi/img_splash_logo.png rename to app/src/main/res/drawable-ko/img_splash_logo.png diff --git a/app/src/main/res/drawable-xhdpi/img_splash_logo.png b/app/src/main/res/drawable-xhdpi/img_splash_logo.png deleted file mode 100644 index aa7878f515f6afc224ea2296e0756ef07087322d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11822 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 diff --git a/app/src/main/res/drawable/img_splash_logo.png b/app/src/main/res/drawable/img_splash_logo.png index a562faec2297441c63e01e91612d6820b714f810..568df3be1ac2195a4e3747730dec5e42becfb303 100644 GIT binary patch literal 14774 zcmdUWWmHvN_wNCuOHi5v($ZarQjjhQDd|Q^;2gR{@z5nDDH4Y+>F$&UDe3O+x|`>D z|L?f>e!ZWrV=z2>?={!#`I~#~wSwQO$l_v=V}U>*TzR=S>L3tO7Vx)@fdAeTVLEWQ+L$v{{T~an|MFg9#aOQ6OD=S4(v2CXX6DnTS>v zkzF&m7me~SQUh8ntlwW_V5V#lkFpnwQC}DHTijUa9OuV5xbk2WBE;^V_1=eS5&k^^ zp~FU8u&qfHI?)AEYH_&uX6z=^b<=({x4-2+F9DWl@btW|@$ytThi@D>KHM+9jrl4p zYQ%86*D#iFBk1T>(i{=Mbt`FPrnq{=>=CnpSk->#YuQcMt2k27epG*|`}f$H_t ziH}bJl_F)C)HQ!j%-WbYtVZ)46uW~e>~CF`2i+3;DbP!JY(}-6!NMPe=him zLY?1KS;hf$1mV+*)B#?Vg{sK|u1n3}sl(}v|#BCaB z`7(8o0t8~(YD2x+92rTp(@#aD;+c>-*o72pZ4s-2PabEz{MNZ^wkdRCsT_W9qfIz$^0Gs*z3XCGnlC+2e z7#QSF))IhL!M~TD=9jmF1o{mk=KFygf^Uwr-g93d=m;hv&UmLdi2_PQ+8y>6VAh&J z`i&p&ovDlkf5IikFMcr9XjJj)HDG@0x_b&zX&kT(*QG463%3OlmnfZJwUx3-)Vi(DsHF2ff<1O zgG~?s>5&OANz{almW8zS+UOYwWZ43kH(Ej{anH)R@}}p~K{rexCP+34WkN0yeu>$pqNs zpmqFu-%NJvW{IXT3u=%hnux#;jREXN7D2YwXx&!*t zRq&p3Ol_JVsTwdjM?a_(S!dBdK1*Ab#tRrn&aHcVz--j`j;;CnImD!6@wl*Oi|w7}%&VBw{$ALV~jS zTX|u>Ac*Y?)HC8BWY{ydCtuWH5XrbJOR>g?T0)40PqLq6%pKJ4uSPHv6vKJ3^xohe z$(XT6v_5m0w!#!_1Qi4MW166XXUa#x)QN2m2O5e9#x=m}_D49yq4&8NNvJ4B$0H%&#xHiD9O^onixaIWz)Y!G?OeoKpN z2?j2McpB=aLLEbhLEckIFau0sk{9F-3MGAdOgH^SXHpQ9`^xu=k~Zy^VSi7sOx-+^ zf53$d##I8F^CHs-5>?bac}^yB#)rGsYQM{NzpG9o@0i=7b~Xcv``&}Q!xIUwD`=$O zJ@vy0%Ylp1UU~#I^bW-zCue&`o_uZLcD-7n=JKrMVF@mA_v^tp^%q|--8JXK8Rv(C zXWiolyLfnu{&{t-*ZmLY3$WSUH*H%k2|v^C$Nb_UDU3m5qZ^ssrrUTeND>nB`lFXs zCV47b+RfJ@KCG2o>f8A%@{_Xu12N$dltidDy}23KinfIhr$dIP?4oaEGR!N*&n~+JR+c_r}q|lllx}p$1pU%~$VP#mC=<#TfTFdcX^l2T%X;@nw)&8Lo9%qnduN?fK^vo z;l}EB;&-pLAW3(hD2?0L=_X$)ygG|+Krn39yP45nk;+Cg=`^5QuKf1B?;U=>ZVk8N zkpZDy7`UDX8gEB7{?+K#^m{$&?U~U7Ctwq{fvv%JKAU$t0iu4H~JT<9kooIQ0!2Nd2#4;!?h}VUTY<@+q->b={-$V>aLBt zF*je8>i$(+4Jt5ytnbC3+=BW0e!sJJ>bs-#EDeuQ_x=UQZ*qD(t$ogB)hf(mCIN#$ zEnJ=;I7O*NLT zf@^NOu(%sk;C~^D!LH2q@HMVnLe#GUTU0N=_jY9kU*e&9fp3)hZb?w$_Vi*w$LYtK z{J#{hNL(v$f0GQxjyl`2qz-ZOM{A`>4-`|zGQH0T9aL$buUz=C_lS9KkyR- z>uy~RmqTkN3=GndhzB0Q2=-d`WKz}d!CE*;;Cf5KWZU7$)C8pv*VnG0Lxok){FD{va`HXMB~2NtT`6dWH=Gc#d9ajvC2u0Sjq43DUvZamy~ z$G7K=ao&#|0ODGFcsRXPd}N7>XLEJkZ*<$?EXpA-JN=BOdIe{#L4SjmO!Zh*wV{Y>x}Eqes`5?VQ-F+its}J z&^%5Z&ar4cJsL`>(^2ZMb&XSM4MmdePf%LuwGMWZ*-0oK0)@(ueEc%(oVCN-@^b{0 zlBR<4pK-2iH!{}IkKJ0ybe4eW_bh^OvC55<%v^@rE?uVT;36R()z(98!V|fc2u~--NIsQf&zpIO&APtCu+R` zsSYX?4WH3}V#TC8%@u=Pde;Pf#}dHzgO`^W8PmZRvZcD_ z-!dSc9Ep6t04U}7qSey)N>I{;WDHhH03faPx@jF=)srz-i~+5};?MRp?`K3n6q z_LrC$--AX7K$-Mka!4}i{Ga>ABWDD6ZelF*?jlDF^Ld^QIeE(fs8H@hxS+mF)Sf(*t^ot>K0dPMfn0N@}T=$M7K5fPn@~Ccu(S z&(okjDE{b&t!$F*+Y@6avIaw-A4t>azk-W3F)~yD2!2J42j?=6Mv=UW{X>&N@XBu$ zwXoP2BV+yAnYwA>3=0G~{y^O{7KcM~s&n&xZsZITZzoODFT|Wz@75lh zi!~~crBW5T;ZIgRNo=A_T7L#QiQvI{h6?XMCK+iE>n9u03c0cXJmbdO&8J`42xNaH z#H!_NfT})ex_SuP@jt;(W~L-)aW!GVS7x6+-k7+*q>_CCVn*$b-9$s7p{_e&`H`ar(Q91uXo{rFd;tPU*FC$U@lM#2N!$GT9Hv$71xXuo^AdUi@;_hA z%>6507E%%&^Y^Mpg+hoL1d61@Gf8x?J%x&{MSM^UWgL}8&vOw^W573G3R=Y+LH{y!KOy|Z3-^%ivR!dzZUf%M+gbMAByHtAaeqV+FKmZ zi8PfVbgVa)`o}GMceR4?KCV%5b-FSaPeQWqOSwLTYC!V*+-d5SKK1H@ljVEXGWEmW zayDcQ4$hys#_8Cf%1-nZXHtjlKu&Ks1$`WM;o6!pfwSagWhY2EOQwB8%lhSbve2=K zDcCE;Ho2+Fj00W{-2^G)U9VuQ^V+9mBB`*tq&4wxdL29B&TsUBbsZw}u*=F0sQ-uh zToz)*gFI?+AHABwpK-wXe^D#PCK6S4a&|Sf<`6Pf1utc`(hLNLGE0QI^v>Wr$xw$I z{7F_Aj1&6f-&9>=Zh-fx?0|t~YD3a|7g{w^ZMoFbBexK{`C;E>UG4R;+cE~L3NzOp z=5H~>t7_IyS1IBP;jiraF4#RrVq3xz28*A3PkM3jG||f{Yv=Asj#rw#(v@zC;vZjc zu6>^S`M>?IV&%67I|-_6J?Zi3C2m%2?)7QMIMXP7jGTHQU=Ks->E=j`Fdt5}R&f&A z6%{Kr_5Cb1#)wk;eO9jaxNom9!ftZriP~y}$Zk?>x&56;K*EeMHn?J9iHSFIMe<#8 zXm!gyE5ga^xV99gW<~`OEvJeIaV8Gk{wG~{)MBOm+`R`RC&J-Q2Zi~lfSJtTs!A}C{2ebf=g8N@`#yXa&%W*GYLX$6;EgU$e6ph{uxnAk5?1YGBxS4)j379_n}X}e5H1f!PbpXBSQNgF9eANu;*V|#IAdB zflWMuFeju%Q*c->lCl6BEURB$SDSb&yb&|O$h~%mh&fX+PFg_ds|xl2T%=L!7e5GP z`zoLdIZvrLtK({G;Bop%(DIzasGNAtg`Jh?E}}_A3jc)W`FS+rq;CoYy_bxB^{wA%WeAEh~_kMuWUDk}a=ytdY&SHK5C;lSgm9?}@EMrk)IdiaxoD$1@LRk3xl{F_+V zE=F9qSeQ(ta8nFo{x>gzZMQ*4}hQ@V=bd0V;`u#wl++ z(3nje$!$QN~=sC{e+4?nLd)46#MSnl4Wi$W4w#~M>y-{uiB6N>%w)PV#4r0 ztcEB?Trdc%34M#1@Qs=&cH)P4&=8)Ab)Y8y>p3xwDSp7?lTO@grIP{5_eK@+C0hg# zqmU!a1fXI701|HM{ZpfNChQw?cl7ua)!OI3E)!zgX&a$sJFnHKm4t6Om3c!pf|g?@ z20Aqnv(k^n5jDAKNY?2c0qKn)w4T=UR7uh-U8C?<@Mvv}W)jVPQLaqVM;Bhpg1gXK>~A zPuj=L0OM~a*2P{m@ znD|~!CBGmUc3KVBUo7n{V|mn~R_K4%0)JCqKqOBD;jaIj#4hO(M$lb_wWRI(OaND{ zoA)i1peCohuSmtJVgbVQzdF!N=GI$2+g=u$n9|o*Iuj#~bW<};$`}e&)Dk4o5Z}-* zpOMoG0X*q)1Yg8 z0R#|h?;|j01nH-b6q>kP%@L;lXmYd#9ON&Pj#8=q}lIPXRkB0NxOw5jgjFZ8_GRyqeJz=`wvAobMfV z*tz25+Cc0xB0$8&bskRqY_GikUtdJyuu-XERb>SI7UOm4OlM0ER`!8pWfc9grGjCt z-u^5qei7V0;UtafwO9&}0FlLNlehAZS7zxpmL-d|-3A#%(suOAUFmq%grt=z9A1z9 z7{bRo|5k2*EkN?9j;BI)vEAwG1=at6Y%`m#eLguqjan6BDj5mTgRQL!Ff7}YDa2B2Kw&9@ts!U0gHiQiRpLmfjlh`{(XtfWb#GN@1p# zUwWf!&d%uVfVQsTW!@`t53TFEpSe#N|J657HTgd&RWK5LDig>h9PPBA_+OO|j#98f zBy_hi!)~rQzbn%X>63GT#SfT?g}+~WjXtKCOdq2s^HgXJdEpPzA80;?fJ=csZ@!lF zMf*F%8-!$HkBM6+5vd5rRlW5;0JU7&XkG;=?h-G_UDW7V2hs}>(lbS85uH^a7YpWO z;od*3g=qbB;f`7c3?Yr`(ZZ-ngcbd_@VTXY*zHb!?$3fUpnFjHH}|4*KZUXq}syivPOV?GG9D!CAv15O|ZdPJzfSh_!;rz!j_fpDeCaS5r>hhI5_ukfe4f*D({%*lmk?c*d+UIC06Vz9jtC6d2v z;gd}_LR|VPD5T$teCPRO!{us@4Bq&0R3fqy%3p5iegNYkXR=0BG8>3 zmw4?(khH#*>0P+pXp>6WO?&eF|8P$Z<-K0{Jf_mGuWPY+9RG@M3}B!=hSIOo^}3|( zJclLo-9woE@0A-2r4zosGkR|dC?zc90O7rCzRRUY$mm2l2AEDeyP!-Y^xqEth2IXB zy2PxghMGFga7@qCkyuRDk@>FIpB^8%nbRvhrm@0ZA}83Z81FWz?5Geydf?sn;rO98Istzq zrLSjr&6VSVjG~?f59?g`+hjA%F{eQP06M7Tx!17M^b@X^qf5*JZ5!ntyqjt}S|><9 zzMP&OE6`|Ayza%~qKqz+Mdg{9B9AURc^!ddm)0|^_}OXr0A6BoVj1PB^&6gLhc?B* zBZqWQ5q4j=ji2_yqNVFHbEITAR%T15f1yy@sl z%fZ4rFM3LGjP(DsEMpw^#N@K`anr-y($&q}*a<#++^*w>0wu)IKfBZm(j=$r8hZfu zJUhL00`^!yR6y3f95|ngiPV@|nOh0v!661|vpXL}cWRJ+pw^qDsO8^{9es|VHXbym zqbp!@oACAXMwB3daHksYA)`e3C8P|kF;loxH2txgwxCxi=JT^^8P{7d(yblP@8~&> z)Z(iCkE+1MD->WE+Tx=em!fNZ`jhu5AKGYI<6CK3bAvK#1?#%UlsD#EnMj7`nMzv7zv_&VJ=AG0CM@KA-^Qn`&ZU1$M z*mqS%74VvX3HU&`aOL_+nO-q%3{4ydC3Ubi;6D%L5?B`y3x#c5*x(4O z@AG3@X%s^AQ#?ZSn@SLd3#j*ZPx^87-jwR+fOvq2uWgC9gDMDX(R~~>fVm&%?<(vd zuo?K!@#Wu38T0ou^w+OTl~#qm1BLH-7@ae$brqJlqpj|P9jFdB&Ma-U`m4HpT7|I0uX=Tb4dAZGWH>yJ#y zzp~1;l1(TIz`*UyMH4jH$=mbv6iYb=j8^5zUTMXA%Df7bTCFsnguj^7lskqtSmPin z(BRPY^SB$KG=`z=zWAwqks9iuEnzB>Z$ZE0FLWESZ7+VD#5w+9r9kPA*Jw%?`P)ik zPbnj_w>rzrK0!kyI8j-=phejIikcPwJID7rgErqFMKpMY$wwJ5Y-eDnfifgpn0~0< z4LUDvR=gG9`B9Ey&CtY2W^T>uY%Yc}CXCp1B{o9~&-igCil{Jc(dtJG?UR)#7ZAKc z&9dS~l2(JJFJU8U$^rZale2&k zrti~2V;^QG@?z)HmF5WM)+x?wy`YPsgt_OqYefOur>7=^cs3jb+}U(*Q&?uf^WFvR zN?Dyk+1{NQzaghSIaeVa&?C8_oAgkrnCMG{>hqa=D=H=FYjW8IMU*Xfa$eVibHm|w zjr8cZ6#Id~qGv_M_WY~FQZ6t#7blroX9OReRjh55icJp@9M%_qEFiku? zlZ4--2O#CteN!3xvsF#yYHk=S9yP~@pT%WbKDs6)r7|}#LetxLR`f$WcD;V(5{Hi! z5qoa#2=j}vdTFl`+K@OygWCY#DD3+bYJ$$po-%r00F{oVi)oEJwFkkMn_ifvD7?he zF>1zkagl2$p4#iVxcHzfCqk&fChnA%VU{#X>KkB;-E27Pj7@ieeiPG6 z8mGDPJP>^)JAp1P^QBs72c7XAd+Rn;E#mq45l<_+_P1ARjt>wW!oV z^O)-O#?jl$;uW*p8j!gH{CQ6QvJXs$qTxLm0q5TD%xRD+$v|A#na{{@mg4B%4Vhd& zHBIA8zbc>5^6ONF0)3eHg$3^5OYQyGhS&#$C7%4#?u;R@q@XMi(6$QnVvz7g*v{_G zPVCebTOsdz+H%suZl<^C?Cm2Al>rLjTnc{^P(*})`!;Y{wVladhX-tIpUC+KSZ zo}1XJyf5VUDzX+1BH~Zxb3;y9A6i+V58mHrXeE zp41nIfe&(*9ul|4mOb5AAxdW_U$AKxMrbIS7!Bf7kgBXtcYv6h+h1#hiIdAzTNRwt zV|!ElF@)gn99Gt}Zq#=O?C$Ty$2A$|Cv%_h(bpG9ub}M#9Iv*)v?HGl%~5Vkk_-?~ z@jpv2C#lf7O%zVv7m+!cjeH+|%94DYclU5*XZZxUso*Mjy9JF#HLS1sQQ+uBi2Fqwfx2go%cjwUNzw{*Rj{%-s6vIBP-Za zjyX^{!>=c~g>|*(n`kGoT8Bt&O^ zU7Xs06sqlIQb7B^j*fb-;D85(8u33s6~?`ErSVqDk5`qonp@e5O&L@-Ruej{6B ztfS168ABO>=*3QEKD#H4`V?^!hq|lo$w#>pM}IBo5xo)xr7P4%5PRjLiPnq8=8P`c zxp;!Criei5NbwS%#6Mw?0b>;er_T18TJ2WRE7_lQ8EUmcN8W0zFD62hl9p|q+NV;z z@yGHaC!d?YU*3FnQ((amu8 zoM5)rAiiGg%oEVnShRI7DM7h^v^aB3ZkE6SUYENYJuizHe^{G!vtmxx*a(_qD1}Ut z$FX`uQdrgne96rwZI+|b)=B}sYX2o%y>c4TwSj#W5F>-z-(r3taQa-m2ueP)xT9Oa zjPEmrJCEdEIxf=PEpA?|bd%kLcWI^or{6{6Y}ov!*C2SPj)$c{F@>3;7BLCF0FO>O zhL&{V+HV6P@;ixN`Byrayo*e$4a|wFHiir}+fEp=)k=#%rv$ERxoKSzag(nT7S8@z*cf z1Cf0_jmBv&pKLRq8Y!%+`W@Aw;rY+_q2U|o2;+qJ_H1|kqUwjSc5pB^oa^~I>JB$?@0z46Q?iWt=oq6!ogj-hBgTRX5$S%{C) z?A7JhqJdbKkOB2(vt!9}uuPb+SXEyM&$RZ+~8^Xo^84pCO0JdQg7gS_) z>x3HI`=X5|yl)tl`5g(oB1%pD`s65L<(GOWWVN7mcEBAPyW*0sV^n}|2(d*`o!$HD z>obpa0yV#-C>aYCyxt2w1!^LRUCJA}e&P=X(LN2Caua1ZVZlSkIVBV5ep_}; zhuINJ&0^bA-^=L8UkPQYeg7s!=zXr|C9g~qb`@>$3^s?^I!U;h;knRs!~;I(D+C@x zDItvrr&5YKmp>Vlh~2L$JsR*hqT$e5VfxoOsf9vJabDy~ecSpe>XUQ7eE52#_@QQt zw%6~B5S;VpKD)Zw=;_CMCl0n7tu*}C1dOC%!VvmaZK)AM2dh|Ke1v9t7sT89Z&TLY z%SNC|FM(qgGf#%ySTkA?J9TW9Jx9^6)nA>$TBr?KRzZ03iOyNEkndmPq;g1IsUTJZ z<8x$~BAi=P>vJ8X?(Q_{3Px5K-bz*%(@1~YbBoSIh6#Pw(-SljCtC`^>OuovN!qH_ zYEu0kyQit6Jq1s)xuZo#ZEQYAxM1Mkg@>w^4}O8+`(&5!J~!N!>b{0}&yF~8OGTvH z#H9rXXyF*;riqXo(oQEV1QT7zeVEO)ylG{Cq!GlPIw?e4UfY`dqSU%Zk80|Ad#Uqx zxN-pO{k717X}GN$|8(yfZ}ta~3sg=+BvZfo_%2l?IrN`RaT^hZSv&axvif&{Qpf92 zqDtRjGYZz5MFD6Y2SS%vp-T3==ykk;aQY!%K@_V~dHD7GN|D=@X)13Zq*$eVnM_iM z-Vslp)Z3i)!`1JvRAD&fi35|jw&`fNGu(o3zRUbn8qK9x%at&m%2dneP|DA_7Ape$ z|H>KlC})+_FN&%$p}&QRkqLG_3AB#I=>Sr#w0w(7~0#)K!%MZ|sXfspoq$pu8Z zR&V_tUu;gF_{eWd*I@T{YV^I7TZ7OiJUkm$4H(`UT~wad1HXEI-O7r1UD$X|+az0L z)8OzUqtjYk@z4=BZmH;u4MGW3bT$@k98MwX4qwJW#I3*O?s8%u`qeC^!s zjuPMgLs-H4i=yIp4WeCS#X_8yXn`)gm6CFlko-ztT;}=b0+V)HJ0Ap^5%#ZBnD?TY zQ+p=!LhDI^#rqKq>bQy_l8)gNT_JjEo2fc-aTIEeeZ~BG@tmeN*AVeTUea7@3>;Ad z!M~Q*|*D!d))1H&SSv%hB2OHfx!a9Z}wY0qYttb}Huqrg&wGn&Q7 zq1;jgoZ(5iCfPvE2cVTQrlhf>syK&Q)SO4N~4l-|tT*q~%dQ>Y-Na1+U)>g~uD=->bZsM2388BIXM^t@K_pbCR0)8EYc@ z4TaNk7$+C zjry6SCTS*rQFzwvk1PkzUwX907u-wF5>5e4wunC+OMxl+Cjq6sD72wxr}E_iDjU_< zenZCdXRaGZbJ-**=QbwQDF zu0L}o(~&vXnqK&bE#S;Iy+o8zjC$PY{r*8V9y+t#ka;wYBbLRHe;^1Xu%#4y)di2* z;=|$c)P`3>Er-Jt?sN+zt7IcNV|ISIVy@+Ed`R7q#E?WV2rcm^ImLm<(^JZOmF*~R z0KCV|i|6}eRda*~9o$R4sji67Q-@NB+sE;5)>lQ(9%hJD7OjLft?SE^aO-SQ>)lb6 zk|6x0k|N2)v4L|HzNtAhWY=rnai_5EmUbm2+P%;fcQ=00GW?JWHTC=}k?fJG_N^=b zMxns3;Xe{=Gs^)wN9rESr1tT-M2gF8+sIEt(fcow^39h0V(F7gG{^_Y#Lg(dn1(5m z?J~ptm1?@m>7AtE(=@kQMs7;yC3kG|ALWg_*lzLBJgo~maX4J*_V~2%$TGGslKDR#NL}Mfi1EalZDTs@lE$SVZ5Z9S#J<9O_?pH zxwzkJ*;_RdGk^S`^p9-@n6SU;u0R=XU(sdP5v>kU7A)r_+=7M$Mt0)1Tca&xU8N;( z>|FB1nJFMPzxrk9Ak2eCqPeoAk{s7`;WNJIeETPd5^Ef1>aLz;C3{8?b~7I5_v1<^ zmD-IxSnj3_gXyE}u$fb=n~#3-p58}d6DBq-wNuRCm{N*rZ#zY>A??BW5D6=l-?2bF z%`c+N$^HPSXP0@$blz8TO988=jD3_&N*nD<6r3j$ga`Byn_KtuN(co0H<2tSik9%a zJHFtO#=Q>=8Xr)Ea|%V7{_xAht4vNQjR?HUm-gbqDml-n=4_>S=>Ze+Wiq000}E%< zWt`fst@L-aFSX`uI|K1A?uI$lpeG8a{RSh`^nR_x?Z#Xa6HmnM=eQE>9~cma|TCys(v50gi04PGEqDng2@ zQi_m~3RbLnx4y@eHlfo1^;KzQ+n#9Qg?x?~P-W8ksa9kt7I*+N#Oq|E9OXM8n=C>i z576ba#*Q28XHIM~JllC5j;xZG4JnNM&`wDi^J+yf>#Ge$R`DQA!ES%lLzw8Bcr)H! z3;kkjlKlF}=O43@VL89MXq4xZ-$$d7g4eiUlgbckglStRZt8-G3NV+UnGl@`2cEof z?6{G5j$8L#iw}6zLx|`byF=oR=~u`{1tQ3o1n!h*E>?KQb2TH}d)-%fse!JeSAq;? z67|cGH|!DPpHy(#|-QJxzxZtBg^+w`CWJysBmaYJ{%=^EK9`6n}?d-9Jx)nDC zegi8wYsRa!2YMFaQ@8V85gTD4Y_8AWf0Sv{q8zmjrC>~4BMY>f=Ph$^XRqV065qH@ z=Fx`7^1{EWx{@>({Dv4{*NgP4Ogfag%F{ zJs`QLmeEEqS>;2O&zeR$Z)L)&wvTN!@Op2SzY?)oTDLV0XT?dB@tTm;HEZIinz3&< z^bu0VaJjZK9h@bF8Ot7U$#c5>yaKmQ>jt!K$@zs+LWQ|Ch1yIF1 zuC!#jKHU(hzTTW*!kr`(c^135+@8)xwUhZ5Lm$ldX!rPjZ`?Vq+>(2<*miNAHv%3! z5{s0C+%^#TmQXck(knkM1c_6uXB?L8PF%=3*|ZCO>^2V~u*-O@0=-RzMWyRtES$*$ zDYlhZC)(YLD7O?V+wyly(>W2g2XEbyNO{wpN7rGteaeM#SU|fejd`-d&m0rKgmSvV`+wfq}qzy08 zw;dGw$%u32Ygu@Aksoy<2Y!9N5~7Z_k2_%>OY_hSu76mZwTJRA^D6e6(-?Rp?Snt@ zDccCgaA~Zkt9I1psJQ+Z9P-I%E6wcoUQNw8*>ZPa!IUtEc$X{soemEU`9ji8VLyD9@O(P-F3b*18j3p=vM`p8XY(O=?42 zwW&`<-@80-@Q}LS2_$#rv!%Jmb^8zUG8c3Zh_z+tCesPAi3Bo6-Sq~Vf%~*)AdrkD zu;1K7n{d9<6gcf?4NvIAVLJm(y$S3}O3c!gB?7C^dS$>Fi(e`7j{jbjn{lInKuY0} z+4ILmKb!DCpjYftdtqvXp=2P?4_t8pl_ut+Mh_kks1S!!u`yleH?WY+Zk#6w0{up& z4kcUliT#&Igj%st_wk|qArHiK0XUIl9h#6mugYc#0{Ih{KC-s_1aJXXMUA$1fN`3% znVG<9w%6X<>kp4Bf=1W?Lqg!(45)?e>BiE>mviH|O7fr6^GU!?dp43rjWLY%A+9Yvs`cfnj#5voy24d{Jy+#^z5z dATG8CYH=Hkr|b~y7=RcgFRk*XSkgG)e*yBMMF#)? literal 6135 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 Dp{mRL diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml deleted file mode 100644 index 3cfef1ae..00000000 --- a/app/src/main/res/values-en/strings.xml +++ /dev/null @@ -1,196 +0,0 @@ - - - Clody - - - Sign Up With KaKao - Sign Up With Google - - - To use Clody,\nPlease agree to the terms\nand conditions - Agree to all - (Required) Terms of Service - (Required) Privacy Policy - Next - - - Nice to meet you!\nWhat should I call you? - Please enter your nickname - Next - - - What time would you\nlike us to remind you to write? - %2$s:%3$s %1$s - Save - Skip for now - - - Hello!\nI\'m Lody - I read your journal entries\nand reply with compliments\nand words of encouragement! - - Each reply comes\nwith a lucky four-leaf clover - The more you confide in Lody\nthe luckier your clover becomes - - You can only journal\nfor today and yesterday - Past or future dates aren\'t available,\nDon\'t miss your moment - - Ready to write\nyour first journal?\nI\'ll be waiting! - From your second journal onward,\nyour next clover will take 12 hours to grow - - Next - Get Started - - - %1$d Clovers - %2$d %1$d - - No gratitude entries yet. - There are existing entries in drafts. - %1$d. %2$s - %1$s - - Write a Journal - See my Reply - Continue Writing - - - Send - %1$d %2$d - Please avoid using slang, profanity, or emojis. - Something you\'re grateful for today. - Please enter between 2 and 100 characters. - Add - - - Lody\'s working hard on your reply! - You\'ve got a lucky reply from Lody! - Lody\'s almost done writing your reply\\njust a little longer! - Watch Ad for Instant Reply - Open Reply - - - %1$d %2$d - A lucky reply for %1$d - - - %2$s %1$s - No gratitude entries yet. - %1$s - /%1$s - Reply - - - Settings - Profile and Account - Notification - Notices - Support/Feedback - Terms of Service - Privacy Policy - Version - Fail to Fetch - - - Profile and Account - " " - Edit - Logout - Are you sure you want to delete your account? - Withdraw - - - Notification - Journal Reminder - Draft Reminder - Reminder Time - %2$s:%3$s %1$s - Reply Notification - - - Try Again - Close - - - Update Available - A new version %1$s is available. Do you want to update now? - Update - Later - - Update Required - Please update to version %1$s to continue - Exit App - - Pick up where you left off? - This journal\'s reply time has expired. - Continue - Cancel - - Delete this entry? - You won\'t receive another reply\nif you delete and rewrite your entry. - Delete - Cancel - - Ready to share\nyour journal with Lody? - Sent journals can\'t be edited. - Send - Cancel - - Want to send this later instead? - Your journal will be lost if you exit now. - Exit - Draft - - %1$s, your luck is here! - 1 Four-Leaf Clover - Done - - Log out now? - I\'ll be here, see you again soon! - Logout - Cancel - - Delete your account? - Your journals, replies, and clovers will be\\npermanently deleted and can\'t be restored. - Withdraw - Cancel - - - Continue writing reminders are now on! - Field required to send. - You can write up to 5 entries. - Save Changes - Save your reminder time - - - Change reminder time - Save - - Heads up- drafts expire\\nDon\'t miss Lody\'s reply! - To receive a reminder before the reply deadline,\nconfirm notification permissions. - [Settings > Apps > Clody > Notifications] - Turn on Notifications - Skip for now - - Delete - - View Another Day - Done - - Edit Nickname - Please write without spaces or special characters - Save - - Choose another time - Save - - - AM - PM - %1$d - %1$d - An error occurred while deleting the diary. - Chrome must be installed to log in. - Login is not available on rooted devices for security reasons. - Failed to load data. - An unexpected error has occurred. - diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 00000000..a7ea8898 --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,196 @@ + + + 클로디 + + + 카카오로 로그인 + Sign Up With Google + + + "Clody 이용을 위해\n약관에 동의해 주세요" + 전체 동의하기 + (필수) 서비스 이용약관 + (필수) 개인정보 처리방침 + 다음 + + + 만나서 반가워요!\n어떻게 불러 드릴까요? + 닉네임을 입력해주세요 + 다음 + + + 몇시에 감사일기\n작성 알림을 드릴까요? + %1$s %2$s시 %3$s분 + 완료 + 다음에 설정할게요 + + + 안녕하세요!\n저는 로디라고 해요 + 여러분이 써준 감사일기를 받고,\n칭찬과 응원을 담아 답장을 쓴답니다 + + 답장마다 행운의\n네잎클로버를 함께 드려요 + 하루에 받은 감사의 수가 많을수록\n색이 진한 네잎클로버를 전달해요 + + 오늘과 전날 일기만\n작성할 수 있어요 + 그전이나 다음날의 일기는 작성할 수\n없으니, 잊지 말고 기록해주세요 + + 이제 일기를 써볼까요?\n기다리고 있을게요! + 두번째 일기부터는 네잎클로버를 찾는 데\n12시간이 걸리니 조금만 기다려 주세요 + + 다음 + 시작하기 + + + 클로버 %1$d개 + %1$d년 %2$d월 + + 작성된 감사 일기가 없어요! + 임시저장된 일기가 있어요. + %1$d. %2$s + %1$s요일 + + 일기 쓰기 + 답장 확인 + 이어 쓰기 + + + 보내기 + %1$d월 %2$d일 + 신조어, 비속어, 이모지 작성은 불가능해요 + 일상 속 작은 감사함을 적어보세요 + 2~50자 까지 입력할 수 있어요. + 추가하기 + + + 로디가 열심히 답장을 쓰고 있어요 + 로디가 답장을 거의 다 써가요!\n조금만 기다려주세요 + 로디가 쓴 행운의 답장이 도착했어요! + 광고 보고 바로 답장 받기 + 열어보기 + + + %1$d월 %2$d일 + %1$s님을 위한 행운의 답장 + + + %1$s년 %2$s월 + 작성된 감사일기가 없어요 + %1$s일 + /%1$s + 답장 확인 + + + 설정 + 프로필 및 계정 관리 + 알림 설정 + 공지사항 + 문의/제안하기 + 서비스 이용 약관 + 개인정보 처리방침 + 앱 버전 + 버전 정보를 불러오는데 실패했습니다. + + + 프로필 및 계정 관리 + + 변경하기 + 로그아웃 + 계정을 삭제하시겠어요? + 회원탈퇴 + + + 알림 설정 + 일기 작성 알림 받기 + 이어쓰기 알림 받기 + 알림 시간 + %1$s %2$s시 %3$s분 + 답장 도착 알림 받기 + + + 다시 시도 + 확인 + + + 업데이트 필요 + 새로운 버전 %1$s을 사용할 수 있습니다.\n지금 업데이트하시겠습니까? + 업데이트 + 나중에 + + 필수 업데이트 + 버전 %1$s으로 업데이트가 필요합니다. + 앱 종료 + + 임시저장된 일기를 이어 쓸까요? + 답장 기한이 지나서 답장은 받을 수 없어요. + 이어쓰기 + 아니오 + + 정말 일기를 삭제할까요? + 아직 답장이 오지 않았거나 삭제하고\n다시 작성한 일기는 답장을 받을 수 없어요. + 삭제할래요 + 아니요 + + 일기를 로디에게 보낼까요? + 보낸 일기는 수정이 어려워요. + 보내기 + 취소 + + 지금까지 쓴 일기를 임시저장할까요? + 나가기를 누르면 작성 중인 내용이 모두 사라져요. + 나가기 + 임시저장 + + %1$s님을 위한 행운 도착 + 1개의 네잎클로버 획득 + 확인 + + 로그아웃 하시겠어요? + 기다릴게요, 다음에 다시 만나요! + 로그아웃 + 아니요 + + 서비스를 탈퇴하시겠어요? + 작성하신 일기와 받은 답장 및 클로버가\n 모두 삭제되며 복구할 수 없어요. + 탈퇴할래요 + 아니요 + + + 이어쓰기 알림 설정을 완료했어요. + 최대 5개까지 작성할 수 있어요. + 빈 칸을 채워야 보낼 수 있어요. + 변경을 완료했어요. + 알람 시간 설정을 완료했어요. + + + 발송 시간 변경 + 완료 + + 기한이 지나면\n로디의 답장을 받을 수 없어요! + 답장 마감 전에 일기를 이어쓸 수 있도록\n알려드리기 위해서는 알림 설정이 필요해요. + [설정 > 애플리케이션 > 클로디 > 알림 > 알림표시] + 알림 받기 + 다음에 하기 + + 삭제하기 + + 다른 날짜 보기 + 완료 + + 닉네임 변경 + 특수문자, 띄어쓰기 없이 작성해주세요 + 변경하기 + + 다른 시간 보기 + 완료 + + + 오전 + 오후 + %1$d년 + %1$d월 + 데이터를 불러오는데 실패했습니다. + 알 수 없는 오류가 발생했습니다. + 일기 삭제 중 오류가 발생했습니다. + Chrome이 설치되어 있어야 로그인이 가능합니다. + 루팅된 기기에서는 로그인할 수 없습니다. + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a70be7ec..3cfef1ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,195 +1,196 @@ + - 클로디 + Clody - - 카카오로 로그인 + + Sign Up With KaKao Sign Up With Google - - "Clody 이용을 위해\n약관에 동의해 주세요" - 전체 동의하기 - (필수) 서비스 이용약관 - (필수) 개인정보 처리방침 - 다음 + + To use Clody,\nPlease agree to the terms\nand conditions + Agree to all + (Required) Terms of Service + (Required) Privacy Policy + Next - - 만나서 반가워요!\n어떻게 불러 드릴까요? - 닉네임을 입력해주세요 - 다음 + + Nice to meet you!\nWhat should I call you? + Please enter your nickname + Next - - 몇시에 감사일기\n작성 알림을 드릴까요? - %1$s %2$s시 %3$s분 - 완료 - 다음에 설정할게요 + + What time would you\nlike us to remind you to write? + %2$s:%3$s %1$s + Save + Skip for now - - 안녕하세요!\n저는 로디라고 해요 - 여러분이 써준 감사일기를 받고,\n칭찬과 응원을 담아 답장을 쓴답니다 + + Hello!\nI\'m Lody + I read your journal entries\nand reply with compliments\nand words of encouragement! - 답장마다 행운의\n네잎클로버를 함께 드려요 - 하루에 받은 감사의 수가 많을수록\n색이 진한 네잎클로버를 전달해요 + Each reply comes\nwith a lucky four-leaf clover + The more you confide in Lody\nthe luckier your clover becomes - 오늘과 전날 일기만\n작성할 수 있어요 - 그전이나 다음날의 일기는 작성할 수\n없으니, 잊지 말고 기록해주세요 + You can only journal\nfor today and yesterday + Past or future dates aren\'t available,\nDon\'t miss your moment - 이제 일기를 써볼까요?\n기다리고 있을게요! - 두번째 일기부터는 네잎클로버를 찾는 데\n12시간이 걸리니 조금만 기다려 주세요 + Ready to write\nyour first journal?\nI\'ll be waiting! + From your second journal onward,\nyour next clover will take 12 hours to grow - 다음 - 시작하기 + Next + Get Started - - 클로버 %1$d개 - %1$d년 %2$d월 + + %1$d Clovers + %2$d %1$d - 작성된 감사 일기가 없어요! - 임시저장된 일기가 있어요. + No gratitude entries yet. + There are existing entries in drafts. %1$d. %2$s - %1$s요일 - - 일기 쓰기 - 답장 확인 - 이어 쓰기 - - - 보내기 - %1$d월 %2$d일 - 신조어, 비속어, 이모지 작성은 불가능해요 - 일상 속 작은 감사함을 적어보세요 - 2~50자 까지 입력할 수 있어요. - 추가하기 - - - 로디가 열심히 답장을 쓰고 있어요 - 로디가 답장을 거의 다 써가요!\n조금만 기다려주세요 - 로디가 쓴 행운의 답장이 도착했어요! - 광고 보고 바로 답장 받기 - 열어보기 - - - %1$d월 %2$d일 - %1$s님을 위한 행운의 답장 - - - %1$s년 %2$s월 - 작성된 감사일기가 없어요 - %1$s일 + %1$s + + Write a Journal + See my Reply + Continue Writing + + + Send + %1$d %2$d + Please avoid using slang, profanity, or emojis. + Something you\'re grateful for today. + Please enter between 2 and 100 characters. + Add + + + Lody\'s working hard on your reply! + You\'ve got a lucky reply from Lody! + Lody\'s almost done writing your reply\\njust a little longer! + Watch Ad for Instant Reply + Open Reply + + + %1$d %2$d + A lucky reply for %1$d + + + %2$s %1$s + No gratitude entries yet. + %1$s /%1$s - 답장 확인 - - - 설정 - 프로필 및 계정 관리 - 알림 설정 - 공지사항 - 문의/제안하기 - 서비스 이용 약관 - 개인정보 처리방침 - 앱 버전 - 버전 정보를 불러오는데 실패했습니다. - - - 프로필 및 계정 관리 - - 변경하기 - 로그아웃 - 계정을 삭제하시겠어요? - 회원탈퇴 - - - 알림 설정 - 일기 작성 알림 받기 - 이어쓰기 알림 받기 - 알림 시간 - %1$s %2$s시 %3$s분 - 답장 도착 알림 받기 - - - 다시 시도 - 확인 - - - 업데이트 필요 - 새로운 버전 %1$s을 사용할 수 있습니다.\n지금 업데이트하시겠습니까? - 업데이트 - 나중에 - - 필수 업데이트 - 버전 %1$s으로 업데이트가 필요합니다. - 앱 종료 - - 임시저장된 일기를 이어 쓸까요? - 답장 기한이 지나서 답장은 받을 수 없어요. - 이어쓰기 - 아니오 - - 정말 일기를 삭제할까요? - 아직 답장이 오지 않았거나 삭제하고\n다시 작성한 일기는 답장을 받을 수 없어요. - 삭제할래요 - 아니요 - - 일기를 로디에게 보낼까요? - 보낸 일기는 수정이 어려워요. - 보내기 - 취소 - - 지금까지 쓴 일기를 임시저장할까요? - 나가기를 누르면 작성 중인 내용이 모두 사라져요. - 나가기 - 임시저장 - - %1$s님을 위한 행운 도착 - 1개의 네잎클로버 획득 - 확인 - - 로그아웃 하시겠어요? - 기다릴게요, 다음에 다시 만나요! - 로그아웃 - 아니요 - - 서비스를 탈퇴하시겠어요? - 작성하신 일기와 받은 답장 및 클로버가\n 모두 삭제되며 복구할 수 없어요. - 탈퇴할래요 - 아니요 - - - 이어쓰기 알림 설정을 완료했어요. - 최대 5개까지 작성할 수 있어요. - 빈 칸을 채워야 보낼 수 있어요. - 변경을 완료했어요. - 알람 시간 설정을 완료했어요. - - - 발송 시간 변경 - 완료 - - 기한이 지나면\n로디의 답장을 받을 수 없어요! - 답장 마감 전에 일기를 이어쓸 수 있도록\n알려드리기 위해서는 알림 설정이 필요해요. - [설정 > 애플리케이션 > 클로디 > 알림 > 알림표시] - 알림 받기 - 다음에 하기 - - 삭제하기 - - 다른 날짜 보기 - 완료 - - 닉네임 변경 - 특수문자, 띄어쓰기 없이 작성해주세요 - 변경하기 - - 다른 시간 보기 - 완료 - - - 오전 - 오후 - %1$d년 - %1$d월 - 데이터를 불러오는데 실패했습니다. - 알 수 없는 오류가 발생했습니다. - 일기 삭제 중 오류가 발생했습니다. - Chrome이 설치되어 있어야 로그인이 가능합니다. - 루팅된 기기에서는 로그인할 수 없습니다. + Reply + + + Settings + Profile and Account + Notification + Notices + Support/Feedback + Terms of Service + Privacy Policy + Version + Fail to Fetch + + + Profile and Account + " " + Edit + Logout + Are you sure you want to delete your account? + Withdraw + + + Notification + Journal Reminder + Draft Reminder + Reminder Time + %2$s:%3$s %1$s + Reply Notification + + + Try Again + Close + + + Update Available + A new version %1$s is available. Do you want to update now? + Update + Later + + Update Required + Please update to version %1$s to continue + Exit App + + Pick up where you left off? + This journal\'s reply time has expired. + Continue + Cancel + + Delete this entry? + You won\'t receive another reply\nif you delete and rewrite your entry. + Delete + Cancel + + Ready to share\nyour journal with Lody? + Sent journals can\'t be edited. + Send + Cancel + + Want to send this later instead? + Your journal will be lost if you exit now. + Exit + Draft + + %1$s, your luck is here! + 1 Four-Leaf Clover + Done + + Log out now? + I\'ll be here, see you again soon! + Logout + Cancel + + Delete your account? + Your journals, replies, and clovers will be\\npermanently deleted and can\'t be restored. + Withdraw + Cancel + + + Continue writing reminders are now on! + Field required to send. + You can write up to 5 entries. + Save Changes + Save your reminder time + + + Change reminder time + Save + + Heads up- drafts expire\\nDon\'t miss Lody\'s reply! + To receive a reminder before the reply deadline,\nconfirm notification permissions. + [Settings > Apps > Clody > Notifications] + Turn on Notifications + Skip for now + + Delete + + View Another Day + Done + + Edit Nickname + Please write without spaces or special characters + Save + + Choose another time + Save + + + AM + PM + %1$d + %1$d + An error occurred while deleting the diary. + Chrome must be installed to log in. + Login is not available on rooted devices for security reasons. + Failed to load data. + An unexpected error has occurred. From fdbe733d71181213623f04cdfa38a6d8cf525dca Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 12 Jul 2025 18:49:22 +0900 Subject: [PATCH 215/299] =?UTF-8?q?[CHORE/#293]=20ktlintFormat=EC=9D=84=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/setting/screen/SettingOptionUrls.kt | 14 ++++++-------- .../ui/setting/screen/SettingScreen.kt | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt index 058b9bc9..68649fa2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt @@ -2,24 +2,22 @@ package com.sopt.clody.presentation.ui.setting.screen enum class SettingOptionUrls( val enUrl: String, - val krUrl: String + val krUrl: String, ) { NOTICES_URL( enUrl = "https://tropical-buckthorn-d17.notion.site/Notice-22ae3fedb3f480feb229e7dcc7a23887?source=copy_link", - krUrl = "https://www.notion.so/1c7e3fedb3f48029b36cf9d76c5fb6d6?pvs=21" + krUrl = "https://www.notion.so/1c7e3fedb3f48029b36cf9d76c5fb6d6?pvs=21", ), SUPPORT_FEEDBACK_URL( enUrl = "https://docs.google.com/forms/d/e/1FAIpQLSe1LJg6tYaWBY2ji3O1smCH1ux5ItbVyGVUQko-Mg609Xt9eg/viewform", - krUrl = "https://forms.gle/WnLC7VwHacufVHiv7" + krUrl = "https://forms.gle/WnLC7VwHacufVHiv7", ), TERMS_OF_SERVICE_URL( enUrl = "https://tropical-buckthorn-d17.notion.site/Clody-Terms-of-Use-22ae3fedb3f48092ace1fba817df8605?source=copy_link", - krUrl = "https://www.notion.so/1c7e3fedb3f4802c8db1f3056c03973f?pvs=21" + krUrl = "https://www.notion.so/1c7e3fedb3f4802c8db1f3056c03973f?pvs=21", ), PRIVACY_POLICY_URL( enUrl = "https://tropical-buckthorn-d17.notion.site/Clody-Privacy-Policy-22ae3fedb3f4808ab8dcc8ba60ad6cd6?source=copy_link", - krUrl = "https://www.notion.so/1c7e3fedb3f48024a334c8116255b378?pvs=21" - ) + krUrl = "https://www.notion.so/1c7e3fedb3f48024a334c8116255b378?pvs=21", + ), } - - diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt index 067566ea..f349c69e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt @@ -79,7 +79,7 @@ fun SettingScreen( SettingSeparateLine() - SettingOption(option = stringResource(R.string.setting_option_notification_setting), onClickNotificationSetting,) + SettingOption(option = stringResource(R.string.setting_option_notification_setting), onClickNotificationSetting) SettingOption(option = stringResource(R.string.setting_option_announcement), onClickNotice) SettingOption(option = stringResource(R.string.setting_option_inquiries_suggestions), onClickSupportFeedback) From a0af28ff35b282df13ef4183955ebef7d6a35433 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 14 Jul 2025 19:11:42 +0900 Subject: [PATCH 216/299] =?UTF-8?q?[FEAT/#300]=20=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20UI=EB=A5=BC=20=EC=A0=9C=EC=9E=91=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/component/dialog/InspectionDialog.kt | 105 ++++++++++++++++++ .../res/drawable/img_inspection_dialog.png | Bin 0 -> 15484 bytes 2 files changed, 105 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt create mode 100644 app/src/main/res/drawable/img_inspection_dialog.png diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt new file mode 100644 index 00000000..613f7cf8 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt @@ -0,0 +1,105 @@ +package com.sopt.clody.presentation.ui.component.dialog + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +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.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.sopt.clody.R +import com.sopt.clody.presentation.utils.base.BasePreview +import com.sopt.clody.presentation.utils.base.ClodyPreview +import com.sopt.clody.ui.theme.ClodyTheme + +@Composable +fun InspectionDialog( + onDismiss: () -> Unit, +) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnClickOutside = false, + usePlatformDefaultWidth = false, + ), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)) + .wrapContentSize(Alignment.Center) + .padding(horizontal = 24.dp), + ) { + Card( + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = ClodyTheme.colors.white), + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.img_inspection_dialog), + contentDescription = null, + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = "보다 안정적인 클로디 서비스를 위해\n시스템 점검 중이에요. 곧 다시 만나요!", + color = ClodyTheme.colors.gray03, + textAlign = TextAlign.Center, + style = ClodyTheme.typography.body3Medium, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "점검시간 : ", + color = ClodyTheme.colors.gray04, + textAlign = TextAlign.Center, + style = ClodyTheme.typography.body3Medium, + ) + Spacer(modifier = Modifier.height(24.dp)) + Button( + onClick = onDismiss, + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors(ClodyTheme.colors.mainYellow), + ) { + Text( + text = "확인 후 앱 종료", + color = ClodyTheme.colors.gray02, + style = ClodyTheme.typography.body3SemiBold, + ) + } + } + } + } + } +} + +@ClodyPreview +@Composable +private fun PreviewInspectionDialog() { + BasePreview { + InspectionDialog( + onDismiss = {}, + ) + } +} diff --git a/app/src/main/res/drawable/img_inspection_dialog.png b/app/src/main/res/drawable/img_inspection_dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..892de40a4b8dec196e9bcde355fd9c5b0018c916 GIT binary patch literal 15484 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 Date: Mon, 14 Jul 2025 21:34:55 +0900 Subject: [PATCH 217/299] =?UTF-8?q?[FEAT/#300]=20AppUpdateChecker=EC=97=90?= =?UTF-8?q?=20=EC=A0=90=EA=B2=80=20=EC=83=81=ED=83=9C=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=20=EB=B0=8F=20=EC=A0=90=EA=B2=80=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EB=B0=98=ED=99=98=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isUnderInspection(): 점검 상태인지 확인하는 함수 - getInspectionTimeText(): 점검 시간 텍스트를 반환하는 함수 --- .../remote/appupdate/AppUpdateCheckerImpl.kt | 29 +++++++++++++++++++ .../domain/appupdate/AppUpdateChecker.kt | 2 ++ 2 files changed, 31 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt index ef137767..d41f2a0b 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/appupdate/AppUpdateCheckerImpl.kt @@ -4,6 +4,9 @@ import com.sopt.clody.data.remote.datasource.RemoteConfigDataSource import com.sopt.clody.domain.appupdate.AppUpdateChecker import com.sopt.clody.domain.model.AppUpdateState import com.sopt.clody.domain.util.VersionComparator +import java.time.LocalDateTime +import java.time.format.TextStyle +import java.util.Locale import javax.inject.Inject class AppUpdateCheckerImpl @Inject constructor( @@ -30,4 +33,30 @@ class AppUpdateCheckerImpl @Inject constructor( } } } + + override suspend fun isUnderInspection(): Boolean { + val start = remoteConfigDataSource.getInspectionStart() ?: return false + val end = remoteConfigDataSource.getInspectionEnd() ?: return false + val now = LocalDateTime.now() + return now.isAfter(start) && now.isBefore(end) + } + + override fun getInspectionTimeText(): String? { + val start = remoteConfigDataSource.getInspectionStart() + val end = remoteConfigDataSource.getInspectionEnd() + if (start == null || end == null) return null + + val startText = formatDateTimeWithDayOfWeek(start) + val endText = formatDateTimeWithDayOfWeek(end) + return "$startText ~ $endText" + } + + private fun formatDateTimeWithDayOfWeek(dateTime: LocalDateTime): String { + val dayOfWeek = dateTime.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN) + val month = dateTime.monthValue + val day = dateTime.dayOfMonth + val hour = dateTime.hour.toString().padStart(2, '0') + + return "$month/$day($dayOfWeek) ${hour}시" + } } diff --git a/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt b/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt index e8f3e7d6..de3f683f 100644 --- a/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt +++ b/app/src/main/java/com/sopt/clody/domain/appupdate/AppUpdateChecker.kt @@ -4,4 +4,6 @@ import com.sopt.clody.domain.model.AppUpdateState interface AppUpdateChecker { suspend fun getAppUpdateState(currentVersion: String): AppUpdateState + suspend fun isUnderInspection(): Boolean + fun getInspectionTimeText(): String? } From 99f30c03e3a8a6c9d2007fa0b2f195f22acea114 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 14 Jul 2025 21:37:06 +0900 Subject: [PATCH 218/299] =?UTF-8?q?[FEAT/#300]=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=A0=90=EA=B2=80=20=EC=8B=9C=EC=9E=91/=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=EA=B0=84=20Remote=20Config=20=ED=82=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=8C=8C=EC=8B=B1=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - inspection_start, inspection_end 키 추가 - ISO-8601 형식 문자열을 LocalDateTime으로 파싱하는 getInspectionStart(), getInspectionEnd() 함수 구현 --- .../remote/datasource/RemoteConfigDataSource.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasource/RemoteConfigDataSource.kt b/app/src/main/java/com/sopt/clody/data/remote/datasource/RemoteConfigDataSource.kt index 8561623a..42e12ba6 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasource/RemoteConfigDataSource.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasource/RemoteConfigDataSource.kt @@ -3,6 +3,8 @@ package com.sopt.clody.data.remote.datasource import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.sopt.clody.BuildConfig import kotlinx.coroutines.tasks.await +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter import javax.inject.Inject import javax.inject.Singleton @@ -20,8 +22,22 @@ class RemoteConfigDataSource @Inject constructor( fun getMinimumVersion(): String = remoteConfig.getString(KEY_MINIMUM_VERSION).ifEmpty { BuildConfig.VERSION_NAME } + fun getInspectionStart(): LocalDateTime? = + remoteConfig.getString(KEY_INSPECTION_START).takeIf { it.isNotBlank() }?.let { + runCatching { LocalDateTime.parse(it, formatter) }.getOrNull() + } + + fun getInspectionEnd(): LocalDateTime? = + remoteConfig.getString(KEY_INSPECTION_END).takeIf { it.isNotBlank() }?.let { + runCatching { LocalDateTime.parse(it, formatter) }.getOrNull() + } + companion object { private const val KEY_LATEST_VERSION = "latest_version" private const val KEY_MINIMUM_VERSION = "min_required_version" + private const val KEY_INSPECTION_START = "inspection_start_android" + private const val KEY_INSPECTION_END = "inspection_end_android" + + private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") } } From c4eb6a8cd452daabab8350853f12d5c543d2f2f2 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 14 Jul 2025 21:38:22 +0900 Subject: [PATCH 219/299] =?UTF-8?q?[ADD/#300]=20=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=ED=8C=9D=EC=97=85=20=EB=85=B8=EC=B6=9C=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=83=81=ED=83=9C=20=EB=B0=8F=20=EC=9D=B8=ED=85=90?= =?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 --- .../com/sopt/clody/presentation/ui/splash/SplashContract.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt index 15ce091f..c120e0c0 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt @@ -5,10 +5,11 @@ import com.airbnb.mvrx.MavericksState import com.sopt.clody.domain.model.AppUpdateState class SplashContract { - data class SplashState( val isUserLoggedIn: Boolean? = null, val updateState: AppUpdateState? = null, + val showInspectionDialog: Boolean = false, + val inspectionTimeText: String? = null, ) : MavericksState sealed class SplashIntent { @@ -16,6 +17,7 @@ class SplashContract { data class HandleHardUpdate(val isConfirm: Boolean) : SplashIntent() data object HandleSoftUpdateConfirm : SplashIntent() data object ClearUpdateState : SplashIntent() + data object DismissInspectionDialog : SplashIntent() } sealed interface SplashSideEffect { From 9858bb3f51e6ea4820622c41a23dca3ce29bbf6a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 14 Jul 2025 21:41:23 +0900 Subject: [PATCH 220/299] =?UTF-8?q?[FEAT/#300]=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=A0=90=EA=B2=80=20=EC=A4=91=20=EC=95=B1=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EC=B0=A8=EB=8B=A8=20=EB=B0=8F=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=ED=8C=9D=EC=97=85=20=EB=A1=9C=EC=A7=81=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 - Remote Config로 점검 시간(시작/종료)을 받아 isUnderInspection 여부 판단 - 점검 중이라면 중단하고 다이얼로그 노출 및 제어 로직 추가 - checkVersionAndNavigate를 suspend로 변경 --- .../presentation/ui/splash/SplashViewModel.kt | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt index 327544d4..7b2b144e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt @@ -49,37 +49,51 @@ class SplashViewModel @AssistedInject constructor( is SplashContract.SplashIntent.HandleHardUpdate -> handleHardUpdate(intent) is SplashContract.SplashIntent.HandleSoftUpdateConfirm -> handleSoftUpdateConfirm() is SplashContract.SplashIntent.ClearUpdateState -> clearUpdateState() + is SplashContract.SplashIntent.DismissInspectionDialog -> handleDismissInspection() } } - private fun handleInitSplash(intent: SplashContract.SplashIntent.InitSplash) { + private suspend fun handleInitSplash(intent: SplashContract.SplashIntent.InitSplash) { if (intent.startIntent.hasExtra("google.message_id")) { AmplitudeUtils.trackEvent(AmplitudeConstraints.ALARM) } + if (checkInspectionAndHandle()) return attemptAutoLogin() checkVersionAndNavigate() } + private suspend fun checkInspectionAndHandle(): Boolean { + if (appUpdateChecker.isUnderInspection()) { + val inspectionText = appUpdateChecker.getInspectionTimeText() + setState { + copy( + showInspectionDialog = true, + inspectionTimeText = inspectionText, + ) + } + return true + } + return false + } + private fun attemptAutoLogin() { val isLoggedIn = tokenRepository.getAccessToken().isNotBlank() && tokenRepository.getRefreshToken().isNotBlank() setState { copy(isUserLoggedIn = isLoggedIn) } } - private fun checkVersionAndNavigate() { - viewModelScope.launch { - val updateState = appUpdateChecker.getAppUpdateState(BuildConfig.VERSION_NAME) - setState { copy(updateState = updateState) } + private suspend fun checkVersionAndNavigate() { + val updateState = appUpdateChecker.getAppUpdateState(BuildConfig.VERSION_NAME) + setState { copy(updateState = updateState) } - if (updateState == AppUpdateState.Latest) { - delay(1000) - val isLoggedIn = withState(this@SplashViewModel) { it.isUserLoggedIn } + if (updateState == AppUpdateState.Latest) { + delay(1000) + val isLoggedIn = withState(this@SplashViewModel) { it.isUserLoggedIn } - if (isLoggedIn == true) { - _sideEffects.send(SplashContract.SplashSideEffect.NavigateToHome) - } else { - _sideEffects.send(SplashContract.SplashSideEffect.NavigateToLogin) - } + if (isLoggedIn == true) { + _sideEffects.send(SplashContract.SplashSideEffect.NavigateToHome) + } else { + _sideEffects.send(SplashContract.SplashSideEffect.NavigateToLogin) } } } @@ -100,6 +114,11 @@ class SplashViewModel @AssistedInject constructor( setState { copy(updateState = AppUpdateState.Latest) } } + private suspend fun handleDismissInspection() { + setState { copy(showInspectionDialog = false) } + _sideEffects.send(SplashContract.SplashSideEffect.FinishApp) + } + @AssistedFactory interface Factory : AssistedViewModelFactory { override fun create(state: SplashContract.SplashState): SplashViewModel From 82f6db7e90d51e9553eccb86c8fb092210c8ca79 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 14 Jul 2025 21:42:07 +0900 Subject: [PATCH 221/299] =?UTF-8?q?[ADD/#300]=20inspectionTime=20parameter?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/component/dialog/InspectionDialog.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt index 613f7cf8..19732703 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt @@ -32,6 +32,7 @@ import com.sopt.clody.ui.theme.ClodyTheme @Composable fun InspectionDialog( + inspectionTime: String, onDismiss: () -> Unit, ) { Dialog( @@ -69,7 +70,7 @@ fun InspectionDialog( ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "점검시간 : ", + text = "점검시간 : $inspectionTime", color = ClodyTheme.colors.gray04, textAlign = TextAlign.Center, style = ClodyTheme.typography.body3Medium, @@ -99,6 +100,7 @@ fun InspectionDialog( private fun PreviewInspectionDialog() { BasePreview { InspectionDialog( + inspectionTime = "", onDismiss = {}, ) } From dbbb5e422e8b0e8543a0ae9ae6d36ea710747d2a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 14 Jul 2025 21:42:56 +0900 Subject: [PATCH 222/299] =?UTF-8?q?[FEAT/#300]=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=A0=90=EA=B2=80=20=EC=95=88=EB=82=B4=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20UI=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/clody/presentation/ui/splash/SplashScreen.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt index a14ff51f..71d5c7ce 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt @@ -25,6 +25,7 @@ import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel import com.sopt.clody.R import com.sopt.clody.domain.model.AppUpdateState +import com.sopt.clody.presentation.ui.component.dialog.InspectionDialog import com.sopt.clody.presentation.utils.appupdate.AppUpdateUtils import com.sopt.clody.presentation.utils.base.BasePreview import com.sopt.clody.presentation.utils.base.ClodyPreview @@ -93,6 +94,14 @@ fun SplashRoute( else -> {} } + if (state.showInspectionDialog) { + InspectionDialog( + inspectionTime = state.inspectionTimeText.orEmpty(), + onDismiss = { + viewModel.postIntent(SplashContract.SplashIntent.DismissInspectionDialog) + }, + ) + } SplashScreen() } From f8da74732d428031784f2ec613a38589cd3d6a10 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 14 Jul 2025 23:36:12 +0900 Subject: [PATCH 223/299] =?UTF-8?q?[CHORE/#300]=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95=20=EC=82=AC=ED=95=AD?= =?UTF-8?q?=EC=9D=84=20=EB=B0=98=EC=98=81=ED=95=98=EA=B3=A0,=20=EB=B0=B1?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=EB=A5=BC=20=EB=A7=89=EC=95=84?= =?UTF-8?q?=EC=84=9C=20=ED=99=95=EC=9D=B8=20=EB=B2=84=ED=8A=BC=EC=9D=84=20?= =?UTF-8?q?=EB=88=8C=EB=9F=AC=EC=95=BC=EB=A7=8C=20=EC=95=B1=EC=9D=84=20?= =?UTF-8?q?=EB=82=98=EA=B0=88=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/component/dialog/InspectionDialog.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt index 19732703..fb885bf5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/InspectionDialog.kt @@ -9,7 +9,6 @@ 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.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -39,6 +38,7 @@ fun InspectionDialog( onDismissRequest = onDismiss, properties = DialogProperties( dismissOnClickOutside = false, + dismissOnBackPress = false, usePlatformDefaultWidth = false, ), ) { @@ -46,8 +46,8 @@ fun InspectionDialog( modifier = Modifier .fillMaxSize() .background(Color.Black.copy(alpha = 0.5f)) - .wrapContentSize(Alignment.Center) .padding(horizontal = 24.dp), + contentAlignment = Alignment.Center, ) { Card( shape = RoundedCornerShape(12.dp), @@ -84,7 +84,7 @@ fun InspectionDialog( colors = ButtonDefaults.buttonColors(ClodyTheme.colors.mainYellow), ) { Text( - text = "확인 후 앱 종료", + text = "확인", color = ClodyTheme.colors.gray02, style = ClodyTheme.typography.body3SemiBold, ) From f6ab945fd940e6ba15888dd9ab9e2e20d88306ab Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 14 Jul 2025 23:49:18 +0900 Subject: [PATCH 224/299] =?UTF-8?q?[REFACTOR/#300]=20=EC=8A=A4=ED=94=8C?= =?UTF-8?q?=EB=9E=98=EC=8B=9C=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20=ED=99=95=EC=9D=B8=20=ED=9B=84=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=9C=EC=84=9C=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 --- .../com/sopt/clody/presentation/ui/splash/SplashViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt index 7b2b144e..fe86c62a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt @@ -58,8 +58,8 @@ class SplashViewModel @AssistedInject constructor( AmplitudeUtils.trackEvent(AmplitudeConstraints.ALARM) } if (checkInspectionAndHandle()) return - attemptAutoLogin() checkVersionAndNavigate() + attemptAutoLogin() } private suspend fun checkInspectionAndHandle(): Boolean { From 864dcbda5e957bcaa4bfab5e30d7ebe87141f89f Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 15 Jul 2025 00:06:00 +0900 Subject: [PATCH 225/299] =?UTF-8?q?[MOD/#300]=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=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 versionCode 27 -> 28 versionName 1.3.0 -> 1.4.0 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e43c8252..98b1101a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,8 +25,8 @@ android { applicationId = "com.sopt.clody" minSdk = 28 targetSdk = 35 - versionCode = 27 - versionName = "1.3.0" + versionCode = 28 + versionName = "1.4.0" val kakaoApiKey: String = properties.getProperty("kakao.api.key") val amplitudeApiKey: String = properties.getProperty("amplitude.api.key") val googleAdmobAppId: String = properties.getProperty("GOOGLE_ADMOB_APP_ID", "") From 01f2b679cd2fee0b09e73850c57bdd0658501351 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Tue, 15 Jul 2025 00:12:45 +0900 Subject: [PATCH 226/299] =?UTF-8?q?[REFACTOR/#300]=20RemoteConfig=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EA=B0=84=EA=B2=A9=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 Fetch 주기를 10분에서 0으로 변경. 불상사 방지 --- app/src/main/java/com/sopt/clody/di/AppUpdateModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/di/AppUpdateModule.kt b/app/src/main/java/com/sopt/clody/di/AppUpdateModule.kt index 98fd28c3..dcb8ea68 100644 --- a/app/src/main/java/com/sopt/clody/di/AppUpdateModule.kt +++ b/app/src/main/java/com/sopt/clody/di/AppUpdateModule.kt @@ -20,7 +20,7 @@ object AppUpdateModule { fun provideFirebaseRemoteConfig(): FirebaseRemoteConfig { val remoteConfig = FirebaseRemoteConfig.getInstance() val configSettings = FirebaseRemoteConfigSettings.Builder() - .setMinimumFetchIntervalInSeconds(600L) + .setMinimumFetchIntervalInSeconds(0) .build() remoteConfig.setConfigSettingsAsync(configSettings) return remoteConfig From 535375ac15de917434d5936c9eefae3b4a8bab8c Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 15 Jul 2025 10:36:33 +0900 Subject: [PATCH 227/299] =?UTF-8?q?[CHORE/#293]=20Modifier=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/component/button/GoogleButton.kt | 5 +---- .../ui/auth/component/button/KaKaoButton.kt | 5 +---- .../presentation/ui/login/LoginScreen.kt | 22 ++----------------- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt index 9f9cddc3..bc95ae33 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt @@ -33,10 +33,7 @@ fun GoogleButton( onClick = onClick, colors = ButtonDefaults.buttonColors(containerColor = ClodyTheme.colors.gray08), shape = RoundedCornerShape(10.dp), - modifier = modifier - .fillMaxWidth() - .height(48.dp) - .padding(horizontal = 24.dp), + modifier = modifier.height(48.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/KaKaoButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/KaKaoButton.kt index a1fe84d0..069251cb 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/KaKaoButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/KaKaoButton.kt @@ -33,10 +33,7 @@ fun KaKaoButton( onClick = onClick, colors = ButtonDefaults.buttonColors(containerColor = ClodyTheme.colors.kakaoYellow), shape = RoundedCornerShape(10.dp), - modifier = modifier - .fillMaxWidth() - .height(48.dp) - .padding(horizontal = 24.dp), + modifier = modifier.height(48.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt index e01b25d2..3162eb97 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt @@ -91,31 +91,13 @@ fun LoginScreen( Scaffold( bottomBar = { -// if (currentLang == "en") { -// GoogleButton( -// text = stringResource(R.string.signup_btn_google), -// onClick = onGoogleLoginClick, -// modifier = Modifier -// .fillMaxWidth() -// .navigationBarsPadding() -// .padding(bottom = 40.dp), -// ) -// } else { -// KaKaoButton( -// text = stringResource(id = R.string.signup_btn_kakao), -// onClick = onKaKaoLoginClick, -// modifier = Modifier -// .fillMaxWidth() -// .navigationBarsPadding() -// .padding(bottom = 40.dp), -// ) -// } KaKaoButton( text = stringResource(id = R.string.signup_btn_kakao), onClick = onKaKaoLoginClick, modifier = Modifier - .fillMaxWidth() .navigationBarsPadding() + .fillMaxWidth() + .padding(horizontal = 24.dp) .padding(bottom = 40.dp), ) }, From eb7a018276f28875da1129adbf52233330bbdcc8 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 15 Jul 2025 10:37:19 +0900 Subject: [PATCH 228/299] =?UTF-8?q?[CHORE/#293]=20KtlintFormat=EC=9D=84=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/auth/component/button/GoogleButton.kt | 4 +--- .../presentation/ui/auth/component/button/KaKaoButton.kt | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt index bc95ae33..d5b2b7f1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/GoogleButton.kt @@ -5,9 +5,7 @@ import androidx.compose.foundation.layout.Arrangement 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.layout.width import androidx.compose.foundation.shape.RoundedCornerShape @@ -33,7 +31,7 @@ fun GoogleButton( onClick = onClick, colors = ButtonDefaults.buttonColors(containerColor = ClodyTheme.colors.gray08), shape = RoundedCornerShape(10.dp), - modifier = modifier.height(48.dp) + modifier = modifier.height(48.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/KaKaoButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/KaKaoButton.kt index 069251cb..38791744 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/KaKaoButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/button/KaKaoButton.kt @@ -5,9 +5,7 @@ import androidx.compose.foundation.layout.Arrangement 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.layout.width import androidx.compose.foundation.shape.RoundedCornerShape @@ -33,7 +31,7 @@ fun KaKaoButton( onClick = onClick, colors = ButtonDefaults.buttonColors(containerColor = ClodyTheme.colors.kakaoYellow), shape = RoundedCornerShape(10.dp), - modifier = modifier.height(48.dp) + modifier = modifier.height(48.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, From d1a17aaa3ecf7412e574e54d03e1aee0ec1737d8 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 15 Jul 2025 13:30:30 +0900 Subject: [PATCH 229/299] =?UTF-8?q?[FEAT/#299]=20=ED=95=9C=EA=B5=AD?= =?UTF-8?q?=EC=96=B4=20typo,=20=EC=98=81=EC=96=B4=20typo=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20lineHeight?= =?UTF-8?q?=20=EA=B3=84=EC=82=B0=20=ED=99=95=EC=9E=A5=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20-=20=EB=84=A4=EC=9D=B4=EB=B0=8D=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 --- .../java/com/sopt/clody/ui/theme/Theme.kt | 16 +- .../main/java/com/sopt/clody/ui/theme/Type.kt | 244 +++++++++++++----- 2 files changed, 192 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt b/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt index 6b04409c..c9cafabc 100644 --- a/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt +++ b/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt @@ -3,12 +3,26 @@ package com.sopt.clody.ui.theme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.platform.LocalConfiguration + +@Composable +fun provideTypographyByLocale(): ClodyTypography { + val locale = LocalConfiguration.current.locales[0] + return if (locale.language == "ko") clodyKoreanTypography else clodyEnglishTypography +} @Composable fun ClodyTheme( content: @Composable () -> Unit, ) { - CompositionLocalProvider(content = content) + val colors = defaultClodyColors + val typography = provideTypographyByLocale() + + CompositionLocalProvider( + LocalClodyColors provides colors, + LocalClodyTypography provides typography, + content = content, + ) } object ClodyTheme { diff --git a/app/src/main/java/com/sopt/clody/ui/theme/Type.kt b/app/src/main/java/com/sopt/clody/ui/theme/Type.kt index daa8bfee..bc07b181 100644 --- a/app/src/main/java/com/sopt/clody/ui/theme/Type.kt +++ b/app/src/main/java/com/sopt/clody/ui/theme/Type.kt @@ -7,145 +7,255 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp import com.sopt.clody.R +@Immutable +data class ClodyTypography( + val head1: TextStyle, + val head2: TextStyle, + val head3: TextStyle, + val head3Medium: TextStyle, + val head4: TextStyle, + val body1SemiBold: TextStyle, + val body1Medium: TextStyle, + val body2SemiBold: TextStyle, + val body2Medium: TextStyle, + val body3SemiBold: TextStyle, + val body3Medium: TextStyle, + val body3Regular: TextStyle, + val body4SemiBold: TextStyle, + val body4Medium: TextStyle, + val detail1SemiBold: TextStyle, + val detail1Medium: TextStyle, + val detail1Regular: TextStyle, + val detail2SemiBold: TextStyle, + val detail2Medium: TextStyle, + val letterMedium: TextStyle, +) + +fun TextUnit.lineHeight(ratio: Float): TextUnit = (this.value * ratio).sp + val pretendardFontFamily = FontFamily( Font(R.font.pretendard_medium, FontWeight.Medium, FontStyle.Normal), Font(R.font.pretendard_regular, FontWeight.Normal, FontStyle.Normal), Font(R.font.pretendard_semibold, FontWeight.SemiBold, FontStyle.Normal), ) -private val pretendardTextStyle = TextStyle( +private val pretendardKoreanTextStyle = TextStyle( fontFamily = pretendardFontFamily, letterSpacing = (-0.2).sp, ) -val defaultClodyTypography = ClodyTypography( - head1 = pretendardTextStyle.copy( +private val pretendardEnglishTextStyle = TextStyle( + fontFamily = pretendardFontFamily, +) + +val clodyKoreanTypography = ClodyTypography( + head1 = pretendardKoreanTextStyle.copy( fontSize = 22.sp, fontWeight = FontWeight.SemiBold, - lineHeight = 33.sp, + lineHeight = 22.sp.lineHeight(1.5f), ), - head2 = pretendardTextStyle.copy( + head2 = pretendardKoreanTextStyle.copy( fontSize = 20.sp, fontWeight = FontWeight.SemiBold, - lineHeight = 30.sp, + lineHeight = 20.sp.lineHeight(1.5f), ), - head3 = pretendardTextStyle.copy( + head3 = pretendardKoreanTextStyle.copy( fontSize = 18.sp, fontWeight = FontWeight.SemiBold, - lineHeight = 27.sp, + lineHeight = 18.sp.lineHeight(1.5f), ), - head3Medium = pretendardTextStyle.copy( + head3Medium = pretendardKoreanTextStyle.copy( fontSize = 18.sp, fontWeight = FontWeight.Medium, - lineHeight = 27.sp, + lineHeight = 18.sp.lineHeight(1.5f), ), - head4 = pretendardTextStyle.copy( + head4 = pretendardKoreanTextStyle.copy( fontSize = 17.sp, fontWeight = FontWeight.SemiBold, - lineHeight = 25.5.sp, + lineHeight = 17.sp.lineHeight(1.5f), ), - body1SemiBold = pretendardTextStyle.copy( + body1SemiBold = pretendardKoreanTextStyle.copy( fontSize = 16.sp, fontWeight = FontWeight.SemiBold, - lineHeight = 24.sp, + lineHeight = 16.sp.lineHeight(1.5f), ), - body1Medium = pretendardTextStyle.copy( + body1Medium = pretendardKoreanTextStyle.copy( fontSize = 16.sp, fontWeight = FontWeight.Medium, - lineHeight = 24.sp, + lineHeight = 16.sp.lineHeight(1.5f), ), - body2SemiBold = pretendardTextStyle.copy( + body2SemiBold = pretendardKoreanTextStyle.copy( fontSize = 15.sp, fontWeight = FontWeight.SemiBold, - lineHeight = 22.5.sp, + lineHeight = 15.sp.lineHeight(1.5f), ), - body2Medium = pretendardTextStyle.copy( + body2Medium = pretendardKoreanTextStyle.copy( fontSize = 15.sp, fontWeight = FontWeight.Medium, - lineHeight = 22.5.sp, + lineHeight = 15.sp.lineHeight(1.5f), ), - body3SemiBold = pretendardTextStyle.copy( + body3SemiBold = pretendardKoreanTextStyle.copy( fontSize = 14.sp, fontWeight = FontWeight.SemiBold, - lineHeight = 21.sp, + lineHeight = 14.sp.lineHeight(1.5f), ), - body3Medium = pretendardTextStyle.copy( + body3Medium = pretendardKoreanTextStyle.copy( fontSize = 14.sp, fontWeight = FontWeight.Medium, - lineHeight = 21.sp, + lineHeight = 14.sp.lineHeight(1.5f), ), - body3Regular = pretendardTextStyle.copy( + body3Regular = pretendardKoreanTextStyle.copy( fontSize = 14.sp, fontWeight = FontWeight.Normal, - lineHeight = 21.sp, + lineHeight = 14.sp.lineHeight(1.5f), ), - body4Medium = pretendardTextStyle.copy( + body4SemiBold = pretendardKoreanTextStyle.copy( fontSize = 13.sp, - fontWeight = FontWeight.Medium, - lineHeight = 19.5.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 13.sp.lineHeight(1.5f), ), - body4SemiBold = pretendardTextStyle.copy( + body4Medium = pretendardKoreanTextStyle.copy( fontSize = 13.sp, - fontWeight = FontWeight.SemiBold, - lineHeight = 19.5.sp, + fontWeight = FontWeight.Medium, + lineHeight = 13.sp.lineHeight(1.5f), ), - detail1SemiBold = pretendardTextStyle.copy( + detail1SemiBold = pretendardKoreanTextStyle.copy( fontSize = 12.sp, fontWeight = FontWeight.SemiBold, - lineHeight = 18.sp, + lineHeight = 12.sp.lineHeight(1.5f), ), - detail1Medium = pretendardTextStyle.copy( + detail1Medium = pretendardKoreanTextStyle.copy( fontSize = 12.sp, fontWeight = FontWeight.Medium, - lineHeight = 18.sp, + lineHeight = 12.sp.lineHeight(1.5f), ), - detail1Regular = pretendardTextStyle.copy( + detail1Regular = pretendardKoreanTextStyle.copy( fontSize = 12.sp, fontWeight = FontWeight.Normal, - lineHeight = 18.sp, + lineHeight = 12.sp.lineHeight(1.5f), ), - detail2SemiBold = pretendardTextStyle.copy( + detail2SemiBold = pretendardKoreanTextStyle.copy( fontSize = 10.sp, fontWeight = FontWeight.SemiBold, - lineHeight = 15.sp, + lineHeight = 10.sp.lineHeight(1.5f), ), - detail2Medium = pretendardTextStyle.copy( + detail2Medium = pretendardKoreanTextStyle.copy( fontSize = 10.sp, fontWeight = FontWeight.Medium, - lineHeight = 15.sp, + lineHeight = 10.sp.lineHeight(1.5f), ), - letterMedium = pretendardTextStyle.copy( + letterMedium = pretendardKoreanTextStyle.copy( fontSize = 14.sp, fontWeight = FontWeight.Medium, - lineHeight = 26.6.sp, + lineHeight = 14.sp.lineHeight(1.9f), ), ) -@Immutable -data class ClodyTypography( - val head1: TextStyle, - val head2: TextStyle, - val head3: TextStyle, - val head3Medium: TextStyle, - val head4: TextStyle, - val body1SemiBold: TextStyle, - val body1Medium: TextStyle, - val body2SemiBold: TextStyle, - val body2Medium: TextStyle, - val body3SemiBold: TextStyle, - val body3Medium: TextStyle, - val body3Regular: TextStyle, - val body4SemiBold: TextStyle, - val body4Medium: TextStyle, - val detail1SemiBold: TextStyle, - val detail1Medium: TextStyle, - val detail1Regular: TextStyle, - val detail2SemiBold: TextStyle, - val detail2Medium: TextStyle, - val letterMedium: TextStyle, +val clodyEnglishTypography = ClodyTypography( + head1 = pretendardEnglishTextStyle.copy( + fontSize = 22.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 22.sp.lineHeight(1.4f), + ), + head2 = pretendardEnglishTextStyle.copy( + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 20.sp.lineHeight(1.4f), + ), + head3 = pretendardEnglishTextStyle.copy( + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 18.sp.lineHeight(1.4f), + ), + head3Medium = pretendardEnglishTextStyle.copy( + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + lineHeight = 18.sp.lineHeight(1.4f), + ), + head4 = pretendardEnglishTextStyle.copy( + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 17.sp.lineHeight(1.4f), + ), + body1SemiBold = pretendardEnglishTextStyle.copy( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 16.sp.lineHeight(1.3f), + ), + body1Medium = pretendardEnglishTextStyle.copy( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + lineHeight = 16.sp.lineHeight(1.3f), + ), + body2SemiBold = pretendardEnglishTextStyle.copy( + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 15.sp.lineHeight(1.3f), + ), + body2Medium = pretendardEnglishTextStyle.copy( + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + lineHeight = 15.sp.lineHeight(1.3f), + ), + body3SemiBold = pretendardEnglishTextStyle.copy( + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 14.sp.lineHeight(1.3f), + ), + body3Medium = pretendardEnglishTextStyle.copy( + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + lineHeight = 14.sp.lineHeight(1.3f), + ), + body3Regular = pretendardEnglishTextStyle.copy( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 14.sp.lineHeight(1.3f), + ), + body4SemiBold = pretendardEnglishTextStyle.copy( + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 13.sp.lineHeight(1.3f), + ), + body4Medium = pretendardEnglishTextStyle.copy( + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + lineHeight = 13.sp.lineHeight(1.3f), + ), + detail1SemiBold = pretendardEnglishTextStyle.copy( + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 12.sp.lineHeight(1.3f), + ), + detail1Medium = pretendardEnglishTextStyle.copy( + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + lineHeight = 12.sp.lineHeight(1.3f), + ), + detail1Regular = pretendardEnglishTextStyle.copy( + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + lineHeight = 12.sp.lineHeight(1.3f), + ), + detail2SemiBold = pretendardEnglishTextStyle.copy( + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + lineHeight = 10.sp.lineHeight(1.3f), + ), + detail2Medium = pretendardEnglishTextStyle.copy( + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + lineHeight = 10.sp.lineHeight(1.3f), + ), + letterMedium = pretendardEnglishTextStyle.copy( + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + lineHeight = 14.sp.lineHeight(1.8f), + ), ) -val LocalClodyTypography = staticCompositionLocalOf { defaultClodyTypography } +val LocalClodyTypography = staticCompositionLocalOf { clodyEnglishTypography } From dbe356df16a3665a777a87d08f6ceacb5e9e95fd Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 15 Jul 2025 13:31:17 +0900 Subject: [PATCH 230/299] =?UTF-8?q?[FEAT/#299]=20LanguageProvider=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84=20-=20?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20->?= =?UTF-8?q?=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20-=20?= =?UTF-8?q?=EC=98=A8=EB=B3=B4=EB=94=A9/=EC=84=A4=EC=A0=95=20=EB=8B=89?= =?UTF-8?q?=EB=84=A4=EC=9E=84=2010=EC=9E=90=20->=2015=EC=9E=90=20-=20?= =?UTF-8?q?=EC=9D=BC=EA=B8=B0=20=EC=9E=91=EC=84=B1=20=ED=95=9C=EC=B9=B8=20?= =?UTF-8?q?=EB=8B=B9=2050=EC=9E=90=20->=20100=EC=9E=90=20-=20=EC=9B=B9?= =?UTF-8?q?=EB=B7=B0=20=EB=A7=81=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/di/LanguageModule.kt | 18 ++++++++ .../ui/auth/signup/page/TermsOfServicePage.kt | 4 +- .../clody/presentation/ui/login/LoginType.kt | 5 +++ .../ui/setting/screen/SettingOptionUrls.kt | 10 ++--- .../ui/setting/screen/SettingScreen.kt | 8 ++-- .../utils/language/LanguageProvider.kt | 11 +++++ .../utils/language/LanguageProviderImpl.kt | 41 +++++++++++++++++++ 7 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/di/LanguageModule.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/login/LoginType.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/di/LanguageModule.kt b/app/src/main/java/com/sopt/clody/presentation/di/LanguageModule.kt new file mode 100644 index 00000000..81d46845 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/di/LanguageModule.kt @@ -0,0 +1,18 @@ +package com.sopt.clody.presentation.di + +import com.sopt.clody.presentation.utils.language.LanguageProvider +import com.sopt.clody.presentation.utils.language.LanguageProviderImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface LanguageModule { + + @Binds + fun bindLanguageProvider( + languageProviderImpl: LanguageProviderImpl, + ): LanguageProvider +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt index df40e950..b063098b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt @@ -48,8 +48,8 @@ fun TermsOfServicePage( val isAgreeButtonEnabled = serviceChecked && privacyChecked val currentLang = Locale.getDefault().language - val termsOfService = if (currentLang == "ko") SettingOptionUrls.TERMS_OF_SERVICE_URL.krUrl else SettingOptionUrls.TERMS_OF_SERVICE_URL.enUrl - val privacyPolicy = if (currentLang == "ko") SettingOptionUrls.PRIVACY_POLICY_URL.krUrl else SettingOptionUrls.PRIVACY_POLICY_URL.enUrl + val termsOfService = if (currentLang == "ko") SettingOptionUrls.TERMS_OF_SERVICE_URL.koUrl else SettingOptionUrls.TERMS_OF_SERVICE_URL.enUrl + val privacyPolicy = if (currentLang == "ko") SettingOptionUrls.PRIVACY_POLICY_URL.koUrl else SettingOptionUrls.PRIVACY_POLICY_URL.enUrl Scaffold( topBar = { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginType.kt new file mode 100644 index 00000000..ff6997ab --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginType.kt @@ -0,0 +1,5 @@ +package com.sopt.clody.presentation.ui.login + +enum class LoginType { + KAKAO, GOOGLE +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt index 68649fa2..7560af1e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingOptionUrls.kt @@ -2,22 +2,22 @@ package com.sopt.clody.presentation.ui.setting.screen enum class SettingOptionUrls( val enUrl: String, - val krUrl: String, + val koUrl: String, ) { NOTICES_URL( enUrl = "https://tropical-buckthorn-d17.notion.site/Notice-22ae3fedb3f480feb229e7dcc7a23887?source=copy_link", - krUrl = "https://www.notion.so/1c7e3fedb3f48029b36cf9d76c5fb6d6?pvs=21", + koUrl = "https://www.notion.so/1c7e3fedb3f48029b36cf9d76c5fb6d6?pvs=21", ), SUPPORT_FEEDBACK_URL( enUrl = "https://docs.google.com/forms/d/e/1FAIpQLSe1LJg6tYaWBY2ji3O1smCH1ux5ItbVyGVUQko-Mg609Xt9eg/viewform", - krUrl = "https://forms.gle/WnLC7VwHacufVHiv7", + koUrl = "https://forms.gle/WnLC7VwHacufVHiv7", ), TERMS_OF_SERVICE_URL( enUrl = "https://tropical-buckthorn-d17.notion.site/Clody-Terms-of-Use-22ae3fedb3f48092ace1fba817df8605?source=copy_link", - krUrl = "https://www.notion.so/1c7e3fedb3f4802c8db1f3056c03973f?pvs=21", + koUrl = "https://www.notion.so/1c7e3fedb3f4802c8db1f3056c03973f?pvs=21", ), PRIVACY_POLICY_URL( enUrl = "https://tropical-buckthorn-d17.notion.site/Clody-Privacy-Policy-22ae3fedb3f4808ab8dcc8ba60ad6cd6?source=copy_link", - krUrl = "https://www.notion.so/1c7e3fedb3f48024a334c8116255b378?pvs=21", + koUrl = "https://www.notion.so/1c7e3fedb3f48024a334c8116255b378?pvs=21", ), } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt index f349c69e..16f1c00d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt @@ -31,10 +31,10 @@ fun SettingRoute( settingViewModel: SettingViewModel = hiltViewModel(), ) { val currentLang = Locale.getDefault().language - val notice = if (currentLang == "ko") SettingOptionUrls.NOTICES_URL.krUrl else SettingOptionUrls.NOTICES_URL.enUrl - val supportFeedback = if (currentLang == "ko") SettingOptionUrls.SUPPORT_FEEDBACK_URL.krUrl else SettingOptionUrls.SUPPORT_FEEDBACK_URL.enUrl - val termsOfService = if (currentLang == "ko") SettingOptionUrls.TERMS_OF_SERVICE_URL.krUrl else SettingOptionUrls.TERMS_OF_SERVICE_URL.enUrl - val privacyPolicy = if (currentLang == "ko") SettingOptionUrls.PRIVACY_POLICY_URL.krUrl else SettingOptionUrls.PRIVACY_POLICY_URL.enUrl + val notice = if (currentLang == "ko") SettingOptionUrls.NOTICES_URL.koUrl else SettingOptionUrls.NOTICES_URL.enUrl + val supportFeedback = if (currentLang == "ko") SettingOptionUrls.SUPPORT_FEEDBACK_URL.koUrl else SettingOptionUrls.SUPPORT_FEEDBACK_URL.enUrl + val termsOfService = if (currentLang == "ko") SettingOptionUrls.TERMS_OF_SERVICE_URL.koUrl else SettingOptionUrls.TERMS_OF_SERVICE_URL.enUrl + val privacyPolicy = if (currentLang == "ko") SettingOptionUrls.PRIVACY_POLICY_URL.koUrl else SettingOptionUrls.PRIVACY_POLICY_URL.enUrl val versionInfo by settingViewModel::versionInfo diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt new file mode 100644 index 00000000..1ca49f26 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt @@ -0,0 +1,11 @@ +package com.sopt.clody.presentation.utils.language + +import com.sopt.clody.presentation.ui.login.LoginType +import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls + +interface LanguageProvider { + fun getLoginType(): LoginType + fun getNicknameMaxLength(): Int + fun getDiaryMaxLength(): Int + fun getWebViewUrlFor(option: SettingOptionUrls): String +} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt new file mode 100644 index 00000000..0a1976d2 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt @@ -0,0 +1,41 @@ +package com.sopt.clody.presentation.utils.language + +import android.content.Context +import com.sopt.clody.presentation.ui.login.LoginType +import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.Locale +import javax.inject.Inject + +class LanguageProviderImpl @Inject constructor( + @ApplicationContext private val context: Context, +) : LanguageProvider { + private val locale: Locale + get() = context.resources.configuration.locales[0] + + private fun isKorean(): Boolean = locale.language == LANGUAGE_KO + + override fun getLoginType(): LoginType { + return if (isKorean()) LoginType.KAKAO else LoginType.GOOGLE + } + + override fun getNicknameMaxLength(): Int { + return if (isKorean()) NICKNAME_MAX_LENGTH_KO else NICKNAME_MAX_LENGTH_EN + } + + override fun getDiaryMaxLength(): Int { + return if (isKorean()) DIARY_MAX_LENGTH_KO else DIARY_MAX_LENGTH_EN + } + + override fun getWebViewUrlFor(option: SettingOptionUrls): String { + return if (isKorean()) option.koUrl else option.enUrl + } + + companion object { + const val LANGUAGE_KO = "ko" + const val NICKNAME_MAX_LENGTH_EN = 15 + const val NICKNAME_MAX_LENGTH_KO = 10 + const val DIARY_MAX_LENGTH_EN = 100 + const val DIARY_MAX_LENGTH_KO = 50 + } +} From b21319ff7f6b9e6f185657b52c2ffc6dd7b8f05a Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 15 Jul 2025 14:31:13 +0900 Subject: [PATCH 231/299] =?UTF-8?q?[FEAT/#299]=20=EC=98=81=EC=96=B4=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=EC=9D=BC=20=EA=B2=BD=EC=9A=B0,=20=EA=B5=AC?= =?UTF-8?q?=EA=B8=80=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=EC=9D=84=20=EB=85=B8=EC=B6=9C=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/login/LoginContract.kt | 2 + .../presentation/ui/login/LoginScreen.kt | 41 ++++++++++++------- .../presentation/ui/login/LoginViewModel.kt | 4 ++ 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt index 4e7d228d..1a9da0a9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt @@ -6,11 +6,13 @@ import com.airbnb.mvrx.MavericksState class LoginContract { data class LoginState( + val loginType: LoginType = LoginType.GOOGLE, val isLoading: Boolean = false, val errorMessage: String? = null, ) : MavericksState sealed class LoginIntent { + data object SetLoginType : LoginIntent() data class LoginWithKakao(val context: Context) : LoginIntent() data class LoginWithGoogle(val context: Context) : LoginIntent() data object ClearError : LoginIntent() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt index 3162eb97..249a873c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt @@ -24,6 +24,7 @@ import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.sopt.clody.R +import com.sopt.clody.presentation.ui.auth.component.button.GoogleButton import com.sopt.clody.presentation.ui.auth.component.button.KaKaoButton import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.dialog.FailureDialog @@ -32,7 +33,6 @@ import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage import com.sopt.clody.presentation.utils.extension.repeatOnStarted import com.sopt.clody.ui.theme.ClodyTheme -import java.util.Locale @Composable fun LoginRoute( @@ -59,7 +59,7 @@ fun LoginRoute( } LoginScreen( - isLoading = state.isLoading, + state = state, onKaKaoLoginClick = { viewModel.postIntent(LoginContract.LoginIntent.LoginWithKakao(context)) }, onGoogleLoginClick = { viewModel.postIntent(LoginContract.LoginIntent.LoginWithGoogle(context)) }, ) @@ -74,13 +74,12 @@ fun LoginRoute( @Composable fun LoginScreen( - isLoading: Boolean, + state: LoginContract.LoginState, onKaKaoLoginClick: () -> Unit, onGoogleLoginClick: () -> Unit, ) { val systemUiController = rememberSystemUiController() val backgroundColor = ClodyTheme.colors.white - val currentLang = Locale.getDefault().language LaunchedEffect(Unit) { systemUiController.setStatusBarColor( @@ -91,15 +90,27 @@ fun LoginScreen( Scaffold( bottomBar = { - KaKaoButton( - text = stringResource(id = R.string.signup_btn_kakao), - onClick = onKaKaoLoginClick, - modifier = Modifier - .navigationBarsPadding() - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(bottom = 40.dp), - ) + if (state.loginType == LoginType.KAKAO) { + KaKaoButton( + text = stringResource(id = R.string.signup_btn_kakao), + onClick = onKaKaoLoginClick, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 40.dp), + ) + } else { + GoogleButton( + text = stringResource(id = R.string.signup_btn_google), + onClick = onGoogleLoginClick, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 40.dp), + ) + } }, ) { innerPadding -> Column( @@ -118,7 +129,7 @@ fun LoginScreen( } } - if (isLoading) { + if (state.isLoading) { LoadingScreen() } } @@ -128,7 +139,7 @@ fun LoginScreen( fun LoginScreenPreview() { BasePreview { LoginScreen( - isLoading = false, + state = LoginContract.LoginState(), onKaKaoLoginClick = {}, onGoogleLoginClick = {}, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt index 9f058f24..32ff7083 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -10,6 +10,7 @@ import com.sopt.clody.core.login.LoginSdk import com.sopt.clody.data.remote.dto.request.LoginRequestDto import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository +import com.sopt.clody.presentation.utils.language.LanguageProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -26,6 +27,7 @@ class LoginViewModel @AssistedInject constructor( private val authRepository: AuthRepository, private val tokenRepository: TokenRepository, private val fcmTokenProvider: FcmTokenProvider, + private val languageProvider: LanguageProvider, ) : MavericksViewModel(initialState) { private val _intents = Channel(BUFFERED) @@ -37,6 +39,7 @@ class LoginViewModel @AssistedInject constructor( .receiveAsFlow() .onEach(::handleIntent) .launchIn(viewModelScope) + postIntent(LoginContract.LoginIntent.SetLoginType) } fun postIntent(intent: LoginContract.LoginIntent) { @@ -45,6 +48,7 @@ class LoginViewModel @AssistedInject constructor( private suspend fun handleIntent(intent: LoginContract.LoginIntent) { when (intent) { + is LoginContract.LoginIntent.SetLoginType -> { setState { copy(loginType = languageProvider.getLoginType()) } } is LoginContract.LoginIntent.LoginWithKakao -> loginWithKakao(intent.context) is LoginContract.LoginIntent.LoginWithGoogle -> loginWithGoogle(intent.context) is LoginContract.LoginIntent.ClearError -> setState { copy(errorMessage = null) } From bc017dacc482aefd5637be847c3cbc3a7d9c308e Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 15 Jul 2025 14:41:44 +0900 Subject: [PATCH 232/299] =?UTF-8?q?[FEAT/#299]=20=EC=9B=B9=EB=B7=B0=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=EB=A5=BC=20=EB=B6=84=EA=B8=B0=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/auth/signup/SignUpContract.kt | 3 +++ .../presentation/ui/auth/signup/SignUpScreen.kt | 6 +++--- .../presentation/ui/auth/signup/SignUpViewModel.kt | 12 ++++++++++++ .../ui/auth/signup/page/TermsOfServicePage.kt | 14 ++++++-------- .../ui/setting/screen/SettingScreen.kt | 10 ++++------ .../ui/setting/screen/SettingViewModel.kt | 7 +++++++ 6 files changed, 35 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt index 501a0140..12109814 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt @@ -13,7 +13,9 @@ class SignUpContract { val isLoading: Boolean = false, val errorMessage: String? = null, val serviceChecked: Boolean = false, + val serviceUrl: String = "", val privacyChecked: Boolean = false, + val privacyUrl: String = "", ) : MavericksState { val allChecked: Boolean get() = serviceChecked && privacyChecked @@ -33,6 +35,7 @@ class SignUpContract { data class ToggleAllChecked(val checked: Boolean) : SignUpIntent() data class ToggleServiceChecked(val checked: Boolean) : SignUpIntent() data class TogglePrivacyChecked(val checked: Boolean) : SignUpIntent() + data object SetWebViewUrl : SignUpIntent() data class OpenWebView(val url: String) : SignUpIntent() data object BackToTerms : SignUpIntent() } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt index fb970fd3..e46e9028 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt @@ -68,14 +68,14 @@ fun SignUpScreen( allChecked = state.allChecked, serviceChecked = state.serviceChecked, privacyChecked = state.privacyChecked, + serviceUrl = state.serviceUrl, + privacyUrl = state.privacyUrl, onToggleAll = { onIntent(SignUpContract.SignUpIntent.ToggleAllChecked(it)) }, onToggleService = { onIntent(SignUpContract.SignUpIntent.ToggleServiceChecked(it)) }, onTogglePrivacy = { onIntent(SignUpContract.SignUpIntent.TogglePrivacyChecked(it)) }, onAgreeClick = { onIntent(SignUpContract.SignUpIntent.ProceedTerms) }, navigateToPrevious = navigateToPrevious, - navigateToWebView = { url -> - onIntent(SignUpContract.SignUpIntent.OpenWebView(url)) - }, + navigateToWebView = { url -> onIntent(SignUpContract.SignUpIntent.OpenWebView(url)) }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index feeffc5b..de17badd 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -30,6 +30,7 @@ class SignUpViewModel @AssistedInject constructor( private val tokenRepository: TokenRepository, private val fcmTokenProvider: FcmTokenProvider, private val networkUtil: NetworkUtil, + private val languageProvider: LanguageProvider, ) : MavericksViewModel(initialState) { private val _intents = Channel(BUFFERED) @@ -41,6 +42,7 @@ class SignUpViewModel @AssistedInject constructor( .receiveAsFlow() .onEach(::handleIntent) .launchIn(viewModelScope) + postIntent(SignUpContract.SignUpIntent.SetWebViewUrl) } fun postIntent(intent: SignUpContract.SignUpIntent) { @@ -57,6 +59,7 @@ class SignUpViewModel @AssistedInject constructor( is SignUpContract.SignUpIntent.ToggleAllChecked -> handleToggleAllChecked(intent) is SignUpContract.SignUpIntent.ToggleServiceChecked -> handleToggleServiceChecked(intent) is SignUpContract.SignUpIntent.TogglePrivacyChecked -> handleTogglePrivacyChecked(intent) + is SignUpContract.SignUpIntent.SetWebViewUrl -> setWebViewUrl() is SignUpContract.SignUpIntent.OpenWebView -> handleOpenWebView(intent.url) SignUpContract.SignUpIntent.BackToTerms -> handleBackToTerms() } @@ -103,6 +106,15 @@ class SignUpViewModel @AssistedInject constructor( setState { copy(privacyChecked = intent.checked) } } + private fun setWebViewUrl() { + setState { + copy( + serviceUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.TERMS_OF_SERVICE_URL), + privacyUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.PRIVACY_POLICY_URL), + ) + } + } + private suspend fun handleOpenWebView(url: String) { _sideEffects.send(SignUpContract.SignUpSideEffect.NavigateToWebView(url)) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt index b063098b..6b2dd169 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt @@ -26,18 +26,18 @@ import com.sopt.clody.R import com.sopt.clody.presentation.ui.auth.component.checkbox.CustomCheckbox import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider -import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls import com.sopt.clody.presentation.utils.base.BasePreview import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage import com.sopt.clody.ui.theme.ClodyTheme -import java.util.Locale @Composable fun TermsOfServicePage( allChecked: Boolean, serviceChecked: Boolean, privacyChecked: Boolean, + serviceUrl: String, + privacyUrl: String, onToggleAll: (Boolean) -> Unit, onToggleService: (Boolean) -> Unit, onTogglePrivacy: (Boolean) -> Unit, @@ -46,10 +46,6 @@ fun TermsOfServicePage( navigateToWebView: (String) -> Unit, ) { val isAgreeButtonEnabled = serviceChecked && privacyChecked - val currentLang = Locale.getDefault().language - - val termsOfService = if (currentLang == "ko") SettingOptionUrls.TERMS_OF_SERVICE_URL.koUrl else SettingOptionUrls.TERMS_OF_SERVICE_URL.enUrl - val privacyPolicy = if (currentLang == "ko") SettingOptionUrls.PRIVACY_POLICY_URL.koUrl else SettingOptionUrls.PRIVACY_POLICY_URL.enUrl Scaffold( topBar = { @@ -117,14 +113,14 @@ fun TermsOfServicePage( text = stringResource(R.string.terms_service_use), checked = serviceChecked, onCheckedChange = onToggleService, - onClickMore = { navigateToWebView(termsOfService) }, + onClickMore = { navigateToWebView(serviceUrl) }, ) Spacer(modifier = Modifier.height(24.dp)) TermsCheckboxRow( text = stringResource(R.string.terms_service_privacy), checked = privacyChecked, onCheckedChange = onTogglePrivacy, - onClickMore = { navigateToWebView(privacyPolicy) }, + onClickMore = { navigateToWebView(privacyUrl) }, ) } }, @@ -169,6 +165,8 @@ private fun TermsOfServicePagePreview() { allChecked = false, serviceChecked = false, privacyChecked = false, + serviceUrl = "", + privacyUrl = "", onToggleAll = {}, onToggleService = {}, onTogglePrivacy = {}, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt index 16f1c00d..79fd4c38 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt @@ -30,12 +30,10 @@ fun SettingRoute( navigateToWebView: (String) -> Unit, settingViewModel: SettingViewModel = hiltViewModel(), ) { - val currentLang = Locale.getDefault().language - val notice = if (currentLang == "ko") SettingOptionUrls.NOTICES_URL.koUrl else SettingOptionUrls.NOTICES_URL.enUrl - val supportFeedback = if (currentLang == "ko") SettingOptionUrls.SUPPORT_FEEDBACK_URL.koUrl else SettingOptionUrls.SUPPORT_FEEDBACK_URL.enUrl - val termsOfService = if (currentLang == "ko") SettingOptionUrls.TERMS_OF_SERVICE_URL.koUrl else SettingOptionUrls.TERMS_OF_SERVICE_URL.enUrl - val privacyPolicy = if (currentLang == "ko") SettingOptionUrls.PRIVACY_POLICY_URL.koUrl else SettingOptionUrls.PRIVACY_POLICY_URL.enUrl - + val notice by settingViewModel::noticeUrl + val supportFeedback by settingViewModel::supportFeedbackUrl + val termsOfService by settingViewModel::termsOfServiceUrl + val privacyPolicy by settingViewModel::privacyPolicyUrl val versionInfo by settingViewModel::versionInfo LaunchedEffect(Unit) { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingViewModel.kt index 3944f4e0..e24e922e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingViewModel.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.R +import com.sopt.clody.presentation.utils.language.LanguageProvider import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.launch @@ -16,7 +17,13 @@ import javax.inject.Inject @HiltViewModel class SettingViewModel @Inject constructor( @ApplicationContext private val context: Context, + private val languageProvider: LanguageProvider, ) : ViewModel() { + val noticeUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.NOTICES_URL) + val supportFeedbackUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.SUPPORT_FEEDBACK_URL) + val termsOfServiceUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.TERMS_OF_SERVICE_URL) + val privacyPolicyUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.PRIVACY_POLICY_URL) + var versionInfo by mutableStateOf(null) private set From c65fc30d205e3a43975d19ab0359a0928cb35785 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 15 Jul 2025 15:05:23 +0900 Subject: [PATCH 233/299] =?UTF-8?q?[FEAT/#299]=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=B5=9C=EB=8C=80=20=EA=B8=B8=EC=9D=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=98=81=EC=96=B4=20=EB=B2=84=EC=A0=84=2015=EC=9E=90,=20?= =?UTF-8?q?=ED=95=9C=EA=B5=AD=EC=96=B4=20=EB=B2=84=EC=A0=84=2010=EC=9E=90?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/component/textfield/NickNameTextField.kt | 4 ++-- .../presentation/ui/auth/signup/SignUpContract.kt | 2 ++ .../presentation/ui/auth/signup/SignUpScreen.kt | 1 + .../presentation/ui/auth/signup/SignUpViewModel.kt | 12 +++++++++++- .../presentation/ui/auth/signup/page/NicknamePage.kt | 5 ++++- .../setting/component/NicknameChangeBottomSheet.kt | 7 ++++--- .../ui/setting/component/NicknameChangeTextField.kt | 3 +-- .../ui/setting/screen/AccountManagementScreen.kt | 4 ++++ .../ui/setting/screen/AccountManagementViewModel.kt | 8 ++++++-- .../presentation/ui/setting/screen/SettingScreen.kt | 1 - 10 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/textfield/NickNameTextField.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/textfield/NickNameTextField.kt index a688900f..711fff7d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/textfield/NickNameTextField.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/textfield/NickNameTextField.kt @@ -29,6 +29,7 @@ import com.sopt.clody.ui.theme.ClodyTheme fun NickNameTextField( value: String, onValueChange: (String) -> Unit, + maxLength: Int, isFocused: Boolean, isValid: Boolean, onRemove: () -> Unit, @@ -36,8 +37,6 @@ fun NickNameTextField( modifier: Modifier = Modifier, hint: String = "", ) { - val maxLength = 10 - BasicTextField( value = value, onValueChange = { @@ -102,6 +101,7 @@ fun PreviewNickNameTextField() { NickNameTextField( value = "닉네임", onValueChange = {}, + maxLength = 15, isFocused = false, isValid = true, onRemove = {}, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt index 12109814..746f35e7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt @@ -9,6 +9,7 @@ class SignUpContract { val nickname: String = "", val isNicknameFocused: Boolean = false, val isValidNickname: Boolean = true, + val nicknameMaxLength: Int = 15, val nicknameMessage: String = DEFAULT_NICKNAME_MESSAGE, val isLoading: Boolean = false, val errorMessage: String? = null, @@ -28,6 +29,7 @@ class SignUpContract { sealed class SignUpIntent { data class SetNickname(val value: String) : SignUpIntent() data class SetNicknameFocus(val isFocused: Boolean) : SignUpIntent() + data object SetNicknameMaxLength : SignUpIntent() data object ProceedTerms : SignUpIntent() data class CompleteSignUp(val context: Context) : SignUpIntent() data object ClearError : SignUpIntent() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt index e46e9028..0e744aa3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt @@ -87,6 +87,7 @@ fun SignUpScreen( onBackClick = { onIntent(SignUpContract.SignUpIntent.BackToTerms) }, isLoading = state.isLoading, isValidNickname = state.isValidNickname, + nicknameMaxLength = state.nicknameMaxLength, nicknameMessage = state.nicknameMessage, isFocused = state.isNicknameFocused, onFocusChanged = { onIntent(SignUpContract.SignUpIntent.SetNicknameFocus(it)) }, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index de17badd..56c63c60 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -13,6 +13,8 @@ import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository import com.sopt.clody.presentation.ui.auth.signup.SignUpContract.Companion.DEFAULT_NICKNAME_MESSAGE +import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls +import com.sopt.clody.presentation.utils.language.LanguageProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -42,6 +44,7 @@ class SignUpViewModel @AssistedInject constructor( .receiveAsFlow() .onEach(::handleIntent) .launchIn(viewModelScope) + postIntent(SignUpContract.SignUpIntent.SetNicknameMaxLength) postIntent(SignUpContract.SignUpIntent.SetWebViewUrl) } @@ -53,6 +56,7 @@ class SignUpViewModel @AssistedInject constructor( when (intent) { is SignUpContract.SignUpIntent.SetNickname -> handleSetNickname(intent) is SignUpContract.SignUpIntent.SetNicknameFocus -> handleSetNicknameFocus(intent) + is SignUpContract.SignUpIntent.SetNicknameMaxLength -> setNicknameMaxLength() is SignUpContract.SignUpIntent.ProceedTerms -> handleProceedTerms() is SignUpContract.SignUpIntent.CompleteSignUp -> signUp(intent.context) is SignUpContract.SignUpIntent.ClearError -> clearError() @@ -84,6 +88,10 @@ class SignUpViewModel @AssistedInject constructor( setState { copy(isNicknameFocused = intent.isFocused) } } + private fun setNicknameMaxLength() { + setState { copy(nicknameMaxLength = languageProvider.getNicknameMaxLength()) } + } + private fun handleProceedTerms() { setState { copy(currentStep = SignUpContract.SignUpState.Step.NICKNAME) } } @@ -165,7 +173,9 @@ class SignUpViewModel @AssistedInject constructor( } private fun validateNickname(nickname: String): Boolean { - val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,10}$".toRegex() + val state = withState(this@SignUpViewModel) { it } + setState { copy(nicknameMaxLength = languageProvider.getNicknameMaxLength()) } + val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,${state.nicknameMaxLength}$".toRegex() return nickname.matches(regex) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt index e1f2f9c0..50a591b9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt @@ -38,6 +38,7 @@ import com.sopt.clody.ui.theme.ClodyTheme fun NickNamePage( nickname: String, isValidNickname: Boolean, + nicknameMaxLength: Int, nicknameMessage: String, isLoading: Boolean, isFocused: Boolean, @@ -56,7 +57,7 @@ fun NickNamePage( append(" / ") } withStyle(style = SpanStyle(color = ClodyTheme.colors.gray06)) { - append("10") + append("$nicknameMaxLength") } } @@ -104,6 +105,7 @@ fun NickNamePage( NickNameTextField( value = nickname, onValueChange = onNicknameChange, + maxLength = nicknameMaxLength, hint = stringResource(R.string.nickname_input_hint), isFocused = isFocused, isValid = isValidNickname, @@ -146,6 +148,7 @@ private fun NicknamePagePreview() { NickNamePage( nickname = "클로디", isValidNickname = true, + nicknameMaxLength = 15, nicknameMessage = "사용 가능한 닉네임입니다.", isLoading = false, isFocused = false, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt index 7110b7db..201072b5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -38,6 +37,7 @@ fun NicknameChangeBottomSheet( accountManagementViewModel: AccountManagementViewModel, userName: String, isValidNickname: Boolean, + nicknameMaxLength: Int, nicknameMessage: String, onDismiss: () -> Unit, ) { @@ -47,6 +47,7 @@ fun NicknameChangeBottomSheet( accountManagementViewModel = accountManagementViewModel, userName = userName, isValidNickname = isValidNickname, + nicknameMaxLength = nicknameMaxLength, nicknameMessage = nicknameMessage, onDismiss = onDismiss, ) @@ -60,14 +61,13 @@ fun NicknameChangeBottomSheetItem( accountManagementViewModel: AccountManagementViewModel, userName: String, isValidNickname: Boolean, + nicknameMaxLength: Int, nicknameMessage: String, onDismiss: () -> Unit, ) { var nickname by remember { mutableStateOf(TextFieldValue("")) } var nicknameChangeState by remember { mutableStateOf(false) } var isFocusedState by remember { mutableStateOf(false) } - val userNicknameState by accountManagementViewModel.userNicknameState.collectAsState() - val nicknameMaxLength = 10 Surface { Column( @@ -112,6 +112,7 @@ fun NicknameChangeBottomSheetItem( accountManagementViewModel.validateNickname(nickname.text) nicknameChangeState = it.text.isNotEmpty() }, + nicknameMaxLength = nicknameMaxLength, isFocused = isFocusedState, isValid = isValidNickname, onRemove = { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeTextField.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeTextField.kt index 9e36c7a0..177041c4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeTextField.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeTextField.kt @@ -31,6 +31,7 @@ import com.sopt.clody.ui.theme.ClodyTheme fun NickNameChangeTextField( value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, + nicknameMaxLength: Int, isFocused: Boolean, isValid: Boolean, onRemove: () -> Unit, @@ -38,8 +39,6 @@ fun NickNameChangeTextField( modifier: Modifier = Modifier, hint: String = "", ) { - val nicknameMaxLength = 10 - Box(modifier = modifier) { BasicTextField( value = value, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt index c5be21b1..168dbfbb 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt @@ -51,6 +51,7 @@ fun AccountManagementRoute( var showRevokeDialog by remember { mutableStateOf(false) } val showFailureDialog by accountManagementViewModel.showFailureDialog.collectAsState() val failureDialogMessage by accountManagementViewModel.failureDialogMessage.collectAsState() + val nicknameMaxLength by accountManagementViewModel.nicknameMaxLength.collectAsState() LaunchedEffect(Unit) { accountManagementViewModel.fetchUserInfo() @@ -81,6 +82,7 @@ fun AccountManagementRoute( showNicknameChangeBottomSheet = showNicknameChangeBottomSheet, updateNicknameChangeBottomSheet = { state -> showNicknameChangeBottomSheet = state }, isValidNickname = isValidNickname, + nicknameMaxLength = nicknameMaxLength, nicknameMessage = nicknameMessage, showLogoutDialog = showLogoutDialog, updateLogoutDialog = { state -> showLogoutDialog = state }, @@ -100,6 +102,7 @@ fun AccountManagementScreen( showNicknameChangeBottomSheet: Boolean, updateNicknameChangeBottomSheet: (Boolean) -> Unit, isValidNickname: Boolean, + nicknameMaxLength: Int, nicknameMessage: String, showLogoutDialog: Boolean, updateLogoutDialog: (Boolean) -> Unit, @@ -160,6 +163,7 @@ fun AccountManagementScreen( NicknameChangeBottomSheet( accountManagementViewModel = accountManagementViewModel, userName = (userInfoState as UserInfoState.Success).data.name, + nicknameMaxLength = nicknameMaxLength, isValidNickname = isValidNickname, nicknameMessage = nicknameMessage, onDismiss = { updateNicknameChangeBottomSheet(false) }, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt index fc78e477..26efca67 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt @@ -7,6 +7,7 @@ import com.sopt.clody.data.datastore.TokenDataStore import com.sopt.clody.data.remote.dto.request.ModifyNicknameRequestDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AccountManagementRepository +import com.sopt.clody.presentation.utils.language.LanguageProvider import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR @@ -23,6 +24,7 @@ class AccountManagementViewModel @Inject constructor( private val tokenDataStore: TokenDataStore, private val networkUtil: NetworkUtil, @ApplicationContext private val context: Context, + private val languageProvider: LanguageProvider, ) : ViewModel() { private val _userInfoState = MutableStateFlow(UserInfoState.Idle) val userInfoState: StateFlow = _userInfoState @@ -48,6 +50,9 @@ class AccountManagementViewModel @Inject constructor( private val _failureDialogMessage = MutableStateFlow("") val failureDialogMessage: StateFlow = _failureDialogMessage + private val _nicknameMaxLength = MutableStateFlow(languageProvider.getNicknameMaxLength()) + val nicknameMaxLength: StateFlow = _nicknameMaxLength + private val maxRetryCount = 3 private var retryCount = 0 @@ -107,7 +112,7 @@ class AccountManagementViewModel @Inject constructor( fun validateNickname(nickname: String) { if (nickname.isNotEmpty()) { - val isValid = nickname.matches(Regex(NICKNAME_PATTERN)) + val isValid = nickname.matches(Regex("^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,${_nicknameMaxLength.value}}$")) _isValidNickname.value = isValid _nicknameMessage.value = if (isValid) DEFAULT_NICKNAME_MESSAGE else FAILURE_NICKNAME_MESSAGE } else { @@ -164,7 +169,6 @@ class AccountManagementViewModel @Inject constructor( } companion object { - private const val NICKNAME_PATTERN = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,10}$" private const val DEFAULT_NICKNAME_MESSAGE = "특수문자, 띄어쓰기 없이 작성해주세요" private const val FAILURE_NICKNAME_MESSAGE = "사용할 수 없는 닉네임이에요" } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt index 79fd4c38..291bed09 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt @@ -20,7 +20,6 @@ import com.sopt.clody.presentation.ui.setting.component.SettingVersionInfo import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.ui.theme.ClodyTheme -import java.util.Locale @Composable fun SettingRoute( From 8fc9d6f335d86a11fda722ba6c0796fc6f90b869 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 15 Jul 2025 15:05:49 +0900 Subject: [PATCH 234/299] =?UTF-8?q?[FEAT/#299]=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EC=9D=98=20=EC=B5=9C=EB=8C=80=20=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=EB=A5=BC=20=EC=98=81=EC=96=B4=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?100=EC=9E=90,=20=ED=95=9C=EA=B5=AD=EC=96=B4=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=2050=EC=9E=90=EB=A1=9C=20=EC=84=A4=EC=A0=95=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../writediary/component/textfield/WriteDiaryTextField.kt | 2 +- .../presentation/ui/writediary/screen/WriteDiaryScreen.kt | 6 +++++- .../ui/writediary/screen/WriteDiaryViewModel.kt | 8 ++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt index 1c495420..ab6ccf3f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt @@ -118,7 +118,7 @@ fun WriteDiaryTextField( if (it.length <= maxLength) { onTextChange(it) val textWithoutSpaces = it.replace("\\s".toRegex(), "") - isTextValid = textWithoutSpaces.matches(Regex("^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,50}$")) + isTextValid = textWithoutSpaces.matches(Regex("^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,$maxLength}$")) isTextTooLong = false } else { isTextTooLong = true diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index ffe3d65d..a3b2a9a2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -75,6 +75,7 @@ fun WriteDiaryRoute( val entryToDelete by viewModel::entryToDelete val showDialog by viewModel::showDialog val showExitDialog by viewModel::showExitDialog + val diaryMaxLength by viewModel.diaryMaxLength.collectAsState() LaunchedEffectWhenStarted { viewModel.fetchDraftDiary(year, month, date) @@ -113,6 +114,7 @@ fun WriteDiaryRoute( showFailureDialog = showFailureDialog, failureMessage = failureMessage, showExitDialog = showExitDialog, + diaryMaxLength = diaryMaxLength, onClickBack = { AmplitudeUtils.trackEvent(AmplitudeConstraints.WRITING_DIARY_BACK) if (!viewModel.hasChangedFromInitial()) { @@ -183,6 +185,7 @@ fun WriteDiaryScreen( showEmptyFieldsMessage: Boolean, showDeleteBottomSheet: Boolean, showDialog: Boolean, + diaryMaxLength: Int, onClickBack: () -> Unit, onClickAdd: () -> Unit, onClickRemove: (Int) -> Unit, @@ -265,7 +268,7 @@ fun WriteDiaryScreen( onTextChange = { newText -> onTextChange(index, newText) }, onRemove = { onClickRemove(index) }, isRemovable = entries.size > 1, - maxLength = 50, + maxLength = diaryMaxLength, showWarning = showWarnings[index], ) } @@ -390,6 +393,7 @@ private fun WriteDiaryScreenPreview() { showEmptyFieldsMessage = false, showDeleteBottomSheet = false, showDialog = false, + diaryMaxLength = 100, onClickBack = {}, onClickAdd = {}, onClickRemove = {}, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index 58fcfb80..fc19175d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -12,6 +12,7 @@ import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.usecase.FetchDraftDiaryUseCase import com.sopt.clody.domain.usecase.SaveDraftDiaryUseCase +import com.sopt.clody.presentation.utils.language.LanguageProvider import com.sopt.clody.presentation.utils.network.ErrorMessages import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE @@ -30,6 +31,7 @@ class WriteDiaryViewModel @Inject constructor( private val saveDraftDiaryUseCase: SaveDraftDiaryUseCase, private val networkUtil: NetworkUtil, private val draftRepository: DraftRepository, + private val languageProvider: LanguageProvider, ) : ViewModel() { private val _writeDiaryState = MutableStateFlow(WriteDiaryState.Idle) @@ -67,6 +69,9 @@ class WriteDiaryViewModel @Inject constructor( private var initialEntries: List = emptyList() + private val _diaryMaxLength = MutableStateFlow(languageProvider.getDiaryMaxLength()) + val diaryMaxLength: StateFlow = _diaryMaxLength + fun writeDiary(year: Int, month: Int, day: Int, contents: List) { viewModelScope.launch { if (!networkUtil.isNetworkAvailable()) { @@ -154,7 +159,7 @@ class WriteDiaryViewModel @Inject constructor( private fun isValidEntry(text: String): Boolean { val textWithoutSpaces = text.replace("\\s".toRegex(), "") - return textWithoutSpaces.matches(Regex(ENTRY_REGEX)) + return textWithoutSpaces.matches(Regex("^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣\\W]{2,${_diaryMaxLength.value}$")) } private fun checkLimitMessage() { @@ -250,6 +255,5 @@ class WriteDiaryViewModel @Inject constructor( companion object { const val MAX_ENTRIES = 5 - const val ENTRY_REGEX = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣\\W]{2,50}$" } } From ac6664c8160ee335063e80a266125b05ef0db143 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 16 Jul 2025 11:22:57 +0900 Subject: [PATCH 235/299] =?UTF-8?q?[CHORE/#299]=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EB=B9=97=20=EB=A6=AC=EB=B7=B0=EC=82=AC=ED=95=AD?= =?UTF-8?q?=EC=9D=84=20=EB=B0=98=EC=98=81=ED=95=A9=EB=8B=88=EB=8B=A4.=20-?= =?UTF-8?q?=20=ED=95=98=EB=93=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20=EC=9D=BC=EA=B8=B0=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=ED=95=84=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EA=B7=9C=EC=8B=9D=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/clody/presentation/ui/splash/SplashScreen.kt | 4 ++-- .../ui/writediary/component/textfield/WriteDiaryTextField.kt | 2 +- .../ui/writediary/component/topbar/WriteDiaryTopBar.kt | 2 +- app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt index fc57d17a..c91f02d9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashScreen.kt @@ -116,8 +116,8 @@ fun SplashScreen() { @Composable fun SoftUpdateDialog( latestVersion: String, - onDismiss: () -> Unit, onConfirm: () -> Unit, + onDismiss: () -> Unit, ) { AlertDialog( onDismissRequest = onDismiss, @@ -155,7 +155,7 @@ fun HardUpdateDialog( }, confirmButton = { TextButton(onClick = onConfirm) { - Text(stringResource(R.string.dialog_soft_update_confirm)) + Text(stringResource(R.string.dialog_hard_update_confirm)) } }, dismissButton = { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt index ab6ccf3f..0f0f0a35 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/textfield/WriteDiaryTextField.kt @@ -53,7 +53,7 @@ fun WriteDiaryTextField( ) { var isTextValid by remember { mutableStateOf( - text.replace("\\s".toRegex(), "").matches(Regex("^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣\\W]{2,50}$")), + text.replace("\\s".toRegex(), "").matches(Regex("^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣\\W]{2,$maxLength}$")), ) } var isFocused by remember { mutableStateOf(false) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/topbar/WriteDiaryTopBar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/topbar/WriteDiaryTopBar.kt index f84448ed..ac85584a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/topbar/WriteDiaryTopBar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/topbar/WriteDiaryTopBar.kt @@ -31,7 +31,7 @@ fun WriteDiaryTopBar( IconButton(onClick = onClickBack) { Icon( painter = painterResource(id = R.drawable.ic_nickname_back), - contentDescription = "뒤로가기", + contentDescription = null, tint = ClodyTheme.colors.gray01, ) } diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index a7ea8898..46e4a0c4 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -118,6 +118,7 @@ 필수 업데이트 버전 %1$s으로 업데이트가 필요합니다. + 업데이트 앱 종료 임시저장된 일기를 이어 쓸까요? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3cfef1ae..5e61e957 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -118,6 +118,7 @@ Update Required Please update to version %1$s to continue + Update Exit App Pick up where you left off? From c28221c68f15c2832a2b3dee2ec924cb465c28a9 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 16 Jul 2025 13:07:03 +0900 Subject: [PATCH 236/299] =?UTF-8?q?[CHORE/#299]=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=82=AC=ED=95=AD=EC=9D=84=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/sopt/clody/ui/theme/Theme.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt b/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt index c9cafabc..c48dab6d 100644 --- a/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt +++ b/app/src/main/java/com/sopt/clody/ui/theme/Theme.kt @@ -3,11 +3,14 @@ package com.sopt.clody.ui.theme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalConfiguration @Composable fun provideTypographyByLocale(): ClodyTypography { - val locale = LocalConfiguration.current.locales[0] + val configuration = LocalConfiguration.current + val locale = remember(configuration) { configuration.locales[0] } + return if (locale.language == "ko") clodyKoreanTypography else clodyEnglishTypography } From 753eb82127f91f6be6717c078531f54a600ace8e Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 16 Jul 2025 15:29:47 +0900 Subject: [PATCH 237/299] =?UTF-8?q?[ADD/#303]=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=95=B1=EC=9D=98=20=EC=95=84=EC=9D=B4=EC=BD=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=82=AC=EC=9A=A9=ED=95=A0=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=97=90=EC=85=8B=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/ic_launcher_dev-playstore.png | Bin 0 -> 19128 bytes .../drawable/ic_launcher_dev_background.xml | 74 ++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher_dev.xml | 5 ++ .../ic_launcher_dev_round.xml | 5 ++ .../main/res/mipmap-hdpi/ic_launcher_dev.webp | Bin 0 -> 1416 bytes .../ic_launcher_dev_foreground.webp | Bin 0 -> 1350 bytes .../mipmap-hdpi/ic_launcher_dev_round.webp | Bin 0 -> 2912 bytes .../main/res/mipmap-mdpi/ic_launcher_dev.webp | Bin 0 -> 1194 bytes .../ic_launcher_dev_foreground.webp | Bin 0 -> 954 bytes .../mipmap-mdpi/ic_launcher_dev_round.webp | Bin 0 -> 1930 bytes .../res/mipmap-xhdpi/ic_launcher_dev.webp | Bin 0 -> 2014 bytes .../ic_launcher_dev_foreground.webp | Bin 0 -> 1776 bytes .../mipmap-xhdpi/ic_launcher_dev_round.webp | Bin 0 -> 3990 bytes .../res/mipmap-xxhdpi/ic_launcher_dev.webp | Bin 0 -> 2898 bytes .../ic_launcher_dev_foreground.webp | Bin 0 -> 2632 bytes .../mipmap-xxhdpi/ic_launcher_dev_round.webp | Bin 0 -> 6294 bytes .../res/mipmap-xxxhdpi/ic_launcher_dev.webp | Bin 0 -> 3958 bytes .../ic_launcher_dev_foreground.webp | Bin 0 -> 3384 bytes .../mipmap-xxxhdpi/ic_launcher_dev_round.webp | Bin 0 -> 8428 bytes 19 files changed, 84 insertions(+) create mode 100644 app/src/main/ic_launcher_dev-playstore.png create mode 100644 app/src/main/res/drawable/ic_launcher_dev_background.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_dev.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_dev_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_dev.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_dev_foreground.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_dev_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_dev.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_dev_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_dev_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_dev.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_dev_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_dev_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_dev.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_dev_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_dev_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_dev.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_dev_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_dev_round.webp diff --git a/app/src/main/ic_launcher_dev-playstore.png b/app/src/main/ic_launcher_dev-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..605ac04d0974ed6900bd5a973fa7500a1ef307f8 GIT binary patch literal 19128 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_dev.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_dev.xml new file mode 100644 index 00000000..a1465e77 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_dev.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_dev_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_dev_round.xml new file mode 100644 index 00000000..a1465e77 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_dev_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_dev.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_dev.webp new file mode 100644 index 0000000000000000000000000000000000000000..19cc7930ee6f3a1d00283c89fe1ef38f2710f049 GIT binary patch literal 1416 zcmV;31$X*VNk&G11pok7MM6+kP&iC;1pojqN5ByfwT6PWZ5Wq7>|F>EF#(W}GNxa= z>U=_uWLtG}1JIu3&$u!q;0&CASbBLV4X5BF6q@YqNnHWWqB&F6lYXb{7 z*_->zkH7=b?I4n5SD7Df?|p)3^q}Gkz{dqL2yk0#+nSuXy9Zh--@wWoN@{vmpiPCu zD%_z8%wPlPI`%Pg+r|+!W$S(S3({?CI|Ju}Y}-a{+qP}nwr!hZ+qRQY@Fwp&H&C}< zYU__2+qP9JTDU}nwA9@_cZUp8_uQcY?pp4(7wpKkO4jn2w*CHWoBem- zHj*N_mU;JX;0^r$?lpik;*OJa2;endP$p`vs6K%**GhOIr7u#1wVXqdG7u#L(K~2< zNwf?^>!A6itMU!D%`dIS&4Wl0ij;n>gvb0^@pWv@y618B*VUPY#wW+2W?fS|d8aZ06aHp zOvkXTE)x0H6FM5cmdTHr($VZ2nf!jAMn=BXR8BwT5{;Bg%q7OCq-*qALNDc@hRB8H zQggiYRtBl&&@#NBSAjYpegl2Augf#Hly5^_)_0Hp$S zP?V?L*HI#I+lbOASp^Vyu-N=U$vAaUf1%z(%^}nXFQjSiDne5&18W^>r0l|_W*Vki zKGCK~-aU#kk}FK;bwAv(!Dk4k)I)Vmza|p{#O* zDW()qtXM`*G-Z6jPgUtIU+@dz{{B|IzEnIuR)6{6MV)MwslxBlA z*B^p<&q33Xk+VV$myX0%dh_={Wpu$ zIP;!f1~{OI!`=kHy$07cgU$>NSbzQ_>QCyXeAS%fWh@W-|Kf$Lp7@`OUBTv=0ZRWb zuXc5V)-5dP{*!tE(n&do-NLcvJadJy+E`&~7;s&5fX(3Y9me}c0E z{jN`+ntEZR`%hiZFLz8#pBWt3IWg7a^g{nH123MIF0F2yo146`Z~O7v-2H8%Ut#v$ zK8SbG|4W_!(&QrW{U7usGIG5pg#R^BU2?5sI+|=M^K?JjgP|`0h0f5GLmKN`p645Z zc?}={7=UA1_vVnKriNYs+0KEnUY08cAjep%y7hA8&?Lx}Ktt~sOXKheIOC}gG=K|G zNwRho*+G#`iY-;FW2h-sIvngL9RP+U0FV^005(7lAP-Ooz3l+NiUS9Lu(XE?ar?Yw W`~B<2_cufDblGsAwgmA%Dh2@lpT2Vd literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_dev_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_dev_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..706633093288333c6f648e3a67933364fb4f7d44 GIT binary patch literal 1350 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 K<<+fh7tM*orks%g literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_dev.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_dev.webp new file mode 100644 index 0000000000000000000000000000000000000000..d8558d302e4b4279d62e05177b4e9077ed98929c GIT binary patch literal 1194 zcmV;b1XcS|Nk&GZ1ONb6MM6+kP&iDM1ONapFTe{B-zT}YZPkj_IZr;&8H3El9SN}85Pg~T@hP5Zr|Nq-wgl|0WpvP5fB3jQ0m?H1fme26Tphq zpilr53d|0x6^x9^G6ETxVgNEI7b7DhQ`+jvMN4!s_HsAk;vlx4V6oy_RTColJ7 zD?COXtpWBnJ$?N39I>5m1%pBm0z@S$EzdTrbhzD#-DtYL$bCbls5BgJJ=`irNQH6R ze-ZFNSK(z_7# z?#i8jNTAib7=a8#1V%&xBT!jJMpz;N_+vC`WMekZ(4eNzQ z0jiJL0u+rht&Lwo1x_w40n>Ot2443_in1B{kkmv8xse;=;q z+}(t`M>u3*+h*v^rrd6w)V6J;R%+WeQ`@N#+ezK6ZQE{Y{S;^u8(bY2))Ox$_&NlAvb@dpv6w8cq)SK~GBMnp!%P?lt|jYl|e zF$aHhHNs}p*(~<3;sCw8Z<0d@S4?sc_Tmzf?&}>22=)xnF%BRmsLde@Ms@2xY<3Xy zozl8@#|a)fdO{-9J-2FQdlQ-NHHO0dEU>G+hA_}Y^lauzaAC6IqT z)gjYKBhX8y5;{r4$~S|Nc#im7tsaP@we({ohgg?E`jRdh{>UL~ zXjT@LmL&D{Bj?#!1r%{BU?|xtrvw@sN^F)3sOR$j2DfF!p;K1EhhR64Q9;gyvR0^= zsa!$$HJnEJz0=#Nh{{*WAi*+AIdM-I;peaoO8Sf{piJ||Ee1Du`*=6m=J6`NY)xHl zyS{DLIYDNA9=x)RT*2bV1P<4*vNr+MI)*#sBKSF3)fGPsst0(tkMw)zHLe^+mS)h( z6XM?u<}tcNIt1qd4I$6Su~4tM?n68D>DQy}h{=onPaEFXwQC&D4u`3X3qcAhOZ!AIpyc*2on-pQ^Eh6 zDgJKXyeU>t#f04UF9rO^EK;TY{r!iue;WO=P{EHw+S2|b*niI9;Y;c?)bVdZLFV{C I9a9oX91(*;(f|Me literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_dev_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_dev_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..ef90b1a5576971d5bd7b8869294b1cb1dfc62a88 GIT binary patch literal 954 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 SK}yAyviJCJ_aS@yzlW^n*lPm- literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_dev_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_dev_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..8457e5a98b81b66fc4cc6e82192657e4885c3f8f GIT binary patch literal 3990 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_dev_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_dev_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..31f4cf3c71fe134524314526e3ed5e3096cbab68 GIT binary patch literal 2632 zcmV-O3b*xANk&FM3IG6CMM6+kP&iC93IG5vL%~oGRax!-eckS6pD;5sGcz+YGcz+Y zGcz+DW@ctS&m(U7-`D-WepmPoswiggc^!L32-7`5P?4j5IN5+yB&iI*@sYd)T4s`w zG{P<8*p*Bfb0kX)kk&$ zW^UcNWVzkomaVI1)THB&tc&a%-GHk*26uoeW@eazD_O{vB-^yDI@dNo^w_rXbZy(V zZQHhQmn7S^P3<}RV4og_wUmrnI*~T8`rEc`+qVBd$=Cl07~AQ8r~jS)clv(}NV7@O z)#*Q{|D1-)Gg*nzwBD6Auo^&w0^vYt5Ks()c7rviRguUi2_fgO>_o$QCt82%h$5s2 zC&GXrVo1>w)~Z%xys&T-fKp_eS|E@L@kOjpjRXhn>18FdP1@y*rt~4vWQy1r8SGJ2 zBF7}Nv9Xfe8(#F$^OFqL0#^_6tH~Xj$_J(2lh1k`^wJ7!ja@p?QX6@_tF+ z_JR4Cso!u3*q~_sA(6b@X#)A5IV736A>AW@Ly3)PG^N1qNz1^s#@zxsFnS7wJ}%I2 zT$W6R2=Dnhf0(BbZj=INqq)3 z1E@4v7@IyZnpACIvj#laS;^W%EaO76CMP*SD8GVPX7XnT#^Lx_KWJ>IR%GIApXcus=lQy zK4cQ{miO1Q=%7T&MB)+>Y4yvKH3N|ng9Zx;=yL~ByU_+m{p}PKbdU9>Mgy>-E2mIkbq}ab^D~*b@NhWP9%(C2 zF!P$t=`KjpwoZXzRDnD!nnTE@hrLp}Pn4Npvn}YLl7IqD^{dbj(6?vvwHewyna%sf z1d!x<>NkiAYR6w#$luhi0o?-kje$z6p9Z^FAC{kC?d(|JN=dQc%*|wFLN6)UIqP&|A^G%C;?eHo1D|+~Tg^Q2qfo@jt--~*2SH=0=IuEYYzh|+ILo1bD+As9YL$?2^ zU@-tnDZk#wy|@`=+eG*!y#uX246p@jDi!_MytSX^va4Bl2B>)2B2zyHjo`u-4c&2Ye?V#C_uOG_Kw zKAY2TSFGU#Iw`)~ghQCwX=jsJztBa*(>}{6GFvsag$O zlD)kOTv={t8mOY&(6*bQj%@#@LVW=hpAd>XPrL|0?yCHIUfm2jGS+FU#&9 zRf^9>LVgOa+rf5Ls>>$=NIU{uz5ABbiq}D6DQF`V7Z>>cNwL;k1&4`(Hr4>UEh`q6 z5YB*t#5%D8YnY1iODY%FtK%?H$et!p)t=sF_2N!26lA$TmF0k%+%nB7_KN{f6eQu@ zgFvOFV4B-7k?nsf8RwcD9z{VCJva(dQwgR2rn=44jK4k-ih|F-|KS$(Krx1>t|;c& zjhgYR(j)Moa7+3OxC1rR&shs9WLyn0W;?tHX1*`tq1-Cqjl;hagKjH4Y;}w zER%tmCp+|0$q~o8QorhY*Jsn;f-B*O;lktq_?Sm?pM$L(+6DE`CmM|nD^KdUz_y?MTEa# zt$*HETZ2YJSCng$C9kgq;#liFEWhNni;~B^xp-NuEGjI-eB1xL3~;lnYcfb(bwTf* z+F3fN#rcbTamQqGz%uTf-AIGMYbxjD?Eggz>&I>Dr?tNDSNA*$1{e3_<{+E;=xCgf znYprydxO?iS+{aaYsYf?0UkwzCux5D#E8WuR;6il`|Quz?6h+qWNEmv(l(b5&B*|X z$6Uyp$EG$<4_seup}*$3hIh{8_wgTF`I${>KfL<24i@(B{O9{saP4SX21!2V&y2J9 qwd>utSlf}+iZ34&{Pu|*c@zL$bo$TfKPS+f{&)J{>3^sHM+*;z;38ZA literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_dev_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_dev_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..c529e029816879dd7cd74be69cbeea7d86dd906d GIT binary patch literal 6294 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 MRVz^^N#Z(I05%R{wMjwr$&t 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 Qt=8_8bQu_s`gL5S1|*ENasU7T literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_dev_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_dev_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..ba5a66091c0c7f1c177fcf034ba27488f1f1aef8 GIT binary patch literal 3384 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 O`=9%t`=9&&aK{0!u5sc3 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_dev_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_dev_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..68293a57083e9811a06b27a6c0d4f4373ef2e663 GIT binary patch literal 8428 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 O(SpbhAtUy)NlpP7)iwM8 literal 0 HcmV?d00001 From ece965335f0387949a2471d01f6c121676657525 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 16 Jul 2025 15:31:35 +0900 Subject: [PATCH 238/299] =?UTF-8?q?[FEAT/#303]=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=95=B1=EC=9D=84=20=EB=B6=84=EB=A6=AC=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20-=20applicationIdSuffix=EB=A1=9C=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B6=84=EB=A6=AC=20-=20appLabel?= =?UTF-8?q?,=20appIcon=20=EB=8B=A4=EB=A5=B4=EA=B2=8C=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20-=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=95=B1=20google-service?= =?UTF-8?q?s.json=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 8 ++++++++ app/src/main/AndroidManifest.xml | 6 +++--- app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 98b1101a..b382f787 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,8 +54,13 @@ android { buildTypes { debug { + applicationIdSuffix = ".dev" + isMinifyEnabled = false buildConfigField("String", "CLODY_BASE_URL", properties["clody.test.url"].toString()) + + manifestPlaceholders["appLabel"] = "@string/app_name_dev" + manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher_dev" } release { @@ -67,6 +72,9 @@ android { "proguard-rules.pro", ) signingConfig = signingConfigs.getByName("release") + + manifestPlaceholders["appLabel"] = "@string/app_name" + manifestPlaceholders["appIcon"] = "@mipmap/ic_launcher" } } compileOptions { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e3e223b2..341bddc1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,12 +13,13 @@ android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:hardwareAccelerated="true" - android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" + android:icon="${appIcon}" + android:label="${appLabel}" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.CLODY" android:usesCleartextTraffic="true" + tools:replace="icon, label" tools:targetApi="31"> diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 46e4a0c4..e5b867f4 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1,6 +1,7 @@ 클로디 + 돈키 카카오로 로그인 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e61e957..870b4a0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ Clody + Donkey Sign Up With KaKao From e6c4779d5347bf2293d3e228a8cc6b60484fbf72 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 16 Jul 2025 15:53:52 +0900 Subject: [PATCH 239/299] =?UTF-8?q?[FEAT/#303]=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=90=9C=20google-services.json=20=ED=8C=8C=EC=9D=BC=EC=97=90?= =?UTF-8?q?=20=EA=B4=80=ED=95=9C=20CI=20=EC=84=A4=EC=A0=95=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/android_ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index 7ab18cd7..9776c421 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -63,6 +63,14 @@ jobs: FIREBASE_SECRET: ${{ secrets.FIREBASE_SECRET }} # base64로 암호화된 json 사용 run: echo $FIREBASE_SECRET | base64 --decode > app/google-services.json + # Firebase dubug google-services.json 복호화 및 설정 + - name: Decode debug google-services.json + env: + FIREBASE_DEBUG_SECRET: ${{ secrets.FIREBASE_DEBUG_SECRET }} + run: | + mkdir -p app/src/debug + echo "$FIREBASE_DEBUG_SECRET" | base64 --decode > app/src/debug/google-services.json + # keystore 복호화 - name: Decode keystore file env: From e8b541e4042ad26c35fd3cfb80b9b7b26c4d8d6d Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 16 Jul 2025 16:16:43 +0900 Subject: [PATCH 240/299] =?UTF-8?q?[FEAT/#303]=20strings.xml=20=EB=A6=AC?= =?UTF-8?q?=EC=86=8C=EC=8A=A4=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20=EC=9A=B0=EC=84=A0?= =?UTF-8?q?=EC=88=9C=EC=9C=84=EB=8A=94=20=EB=B9=8C=EB=93=9C=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20->=20=EB=A1=9C=EC=BC=80=EC=9D=BC=20=EC=88=9C?= =?UTF-8?q?=EC=9E=85=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/debug/res/values-ko/strings.xml | 4 ++++ app/src/debug/res/values/strings.xml | 4 ++++ app/src/main/res/values-ko/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 app/src/debug/res/values-ko/strings.xml create mode 100644 app/src/debug/res/values/strings.xml diff --git a/app/src/debug/res/values-ko/strings.xml b/app/src/debug/res/values-ko/strings.xml new file mode 100644 index 00000000..4128eb16 --- /dev/null +++ b/app/src/debug/res/values-ko/strings.xml @@ -0,0 +1,4 @@ + + + 돈키 + diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100644 index 00000000..9b7d4301 --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Donkey + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index e5b867f4..46e4a0c4 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1,7 +1,6 @@ 클로디 - 돈키 카카오로 로그인 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 870b4a0a..5e61e957 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,6 @@ Clody - Donkey Sign Up With KaKao From 23259d5a40723354c708e02c64aa751798f152ee Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 16 Jul 2025 18:37:52 +0900 Subject: [PATCH 241/299] =?UTF-8?q?[REFACTOR/#303]=20=EB=94=94=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=95=B1=EC=97=90=EC=84=9C=EB=8A=94=20Amplitude=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=EC=9D=84=20=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/sopt/clody/ClodyApplication.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/ClodyApplication.kt b/app/src/main/java/com/sopt/clody/ClodyApplication.kt index def59341..bd8032de 100644 --- a/app/src/main/java/com/sopt/clody/ClodyApplication.kt +++ b/app/src/main/java/com/sopt/clody/ClodyApplication.kt @@ -16,7 +16,10 @@ class ClodyApplication : Application() { initKakaoSdk() FirebaseApp.initializeApp(this) Mavericks.initialize(this) - initAmplitude(applicationContext) + + if (!BuildConfig.DEBUG) { + initAmplitude(applicationContext) + } } private fun initKakaoSdk() { From 84e1ad034817494075913c6dcdcdacd2b8a32d45 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 17 Jul 2025 00:07:59 +0900 Subject: [PATCH 242/299] =?UTF-8?q?[REFACTOR/#303]=20=EB=94=94=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=95=B1=EC=97=90=EC=84=9C=EB=8A=94=20=EC=A0=90?= =?UTF-8?q?=EA=B2=80=EC=8B=9C=EA=B0=84=20=EC=8B=9C=20=EC=95=B1=20=EC=A7=84?= =?UTF-8?q?=EC=9E=85=20=EC=B0=A8=EB=8B=A8=EC=9D=84=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/clody/presentation/ui/splash/SplashViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt index fe86c62a..5d3deac1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt @@ -57,7 +57,7 @@ class SplashViewModel @AssistedInject constructor( if (intent.startIntent.hasExtra("google.message_id")) { AmplitudeUtils.trackEvent(AmplitudeConstraints.ALARM) } - if (checkInspectionAndHandle()) return + if (!BuildConfig.DEBUG && checkInspectionAndHandle()) return checkVersionAndNavigate() attemptAutoLogin() } From d0b558cfc185e13ce961f13aa32d4d786c93302d Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 17 Jul 2025 11:36:12 +0900 Subject: [PATCH 243/299] =?UTF-8?q?[FIX/#306]=20Amplitude=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.=20(=EC=B6=94=ED=9B=84=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=A0=81=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/sopt/clody/ClodyApplication.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/ClodyApplication.kt b/app/src/main/java/com/sopt/clody/ClodyApplication.kt index bd8032de..def59341 100644 --- a/app/src/main/java/com/sopt/clody/ClodyApplication.kt +++ b/app/src/main/java/com/sopt/clody/ClodyApplication.kt @@ -16,10 +16,7 @@ class ClodyApplication : Application() { initKakaoSdk() FirebaseApp.initializeApp(this) Mavericks.initialize(this) - - if (!BuildConfig.DEBUG) { - initAmplitude(applicationContext) - } + initAmplitude(applicationContext) } private fun initKakaoSdk() { From 3d14377216c14efd501a84cba0400c9fb25cb466 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 21 Jul 2025 15:16:52 +0900 Subject: [PATCH 244/299] =?UTF-8?q?[REFACTOR/#305]=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=ED=83=91=EB=B0=94=EC=9D=98=20=EC=97=B0/=EC=9B=94?= =?UTF-8?q?=20=ED=8F=AC=EB=A7=B7=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/component/HomeTopAppBar.kt | 37 ++++++++++---- .../ui/home/component/YearAndMonthTitle.kt | 49 ------------------- .../presentation/ui/home/screen/HomeScreen.kt | 8 +-- .../utils/extension/IntExtension.kt | 23 +++++++++ .../utils/extension/YearMonthLabelUtil.kt | 10 ---- app/src/main/res/values-ko/strings.xml | 6 +-- app/src/main/res/values/strings.xml | 6 +-- 7 files changed, 62 insertions(+), 77 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/extension/IntExtension.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt index 264207a8..6bac1a49 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt @@ -1,16 +1,22 @@ package com.sopt.clody.presentation.ui.home.component import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember 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.unit.dp import com.sopt.clody.R import com.sopt.clody.ui.theme.ClodyTheme @@ -21,21 +27,34 @@ fun HomeTopAppBar( onClickDiaryList: () -> Unit, onClickSetting: () -> Unit, onShowYearMonthPickerStateChange: (Boolean) -> Unit, - selectedYear: Int, - selectedMonth: Int, + selectedYear: String, + selectedMonth: String, ) { CenterAlignedTopAppBar( title = { Box( - modifier = Modifier - .padding(start = 16.dp), + modifier = Modifier.padding(start = 16.dp), contentAlignment = Alignment.Center, ) { - YearAndMonthTitle( - onShowYearMonthPickerStateChange, - selectedYear, - selectedMonth, - ) + Row( + modifier = Modifier.clickable( + onClick = { onShowYearMonthPickerStateChange(true) }, + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.home_year_month_format, selectedYear, selectedMonth), + style = ClodyTheme.typography.head4, + color = ClodyTheme.colors.gray01, + ) + Image( + painter = painterResource(id = R.drawable.ic_home_under_arrow), + contentDescription = "choose month", + modifier = Modifier.padding(horizontal = 6.dp, vertical = 6.dp), + ) + } } }, navigationIcon = { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt deleted file mode 100644 index 209515cd..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/YearAndMonthTitle.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.sopt.clody.presentation.ui.home.component - -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -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.unit.dp -import com.sopt.clody.R -import com.sopt.clody.ui.theme.ClodyTheme - -@Composable -fun YearAndMonthTitle( - onShowYearMonthPickerStateChange: (Boolean) -> Unit, - selectedYear: Int, - selectedMonth: Int, -) { - val text = stringResource(R.string.home_year_month_format, selectedYear, selectedMonth) - - Column { - Row( - modifier = Modifier.clickable( - onClick = { onShowYearMonthPickerStateChange(true) }, - indication = null, - interactionSource = remember { MutableInteractionSource() }, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = text, - style = ClodyTheme.typography.head4, - color = ClodyTheme.colors.gray01, - ) - Image( - painter = painterResource(id = R.drawable.ic_home_under_arrow), - contentDescription = "choose month", - modifier = Modifier.padding(horizontal = 6.dp, vertical = 6.dp), - ) - } - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 693a5fab..71956140 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -45,6 +45,8 @@ import com.sopt.clody.presentation.ui.home.component.DiaryStateButton import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel +import com.sopt.clody.presentation.utils.extension.toLocalizedYearLabel import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.coroutines.async @@ -345,8 +347,8 @@ fun HomeScreen( }, onClickSetting = onClickSetting, onShowYearMonthPickerStateChange = { newState -> homeViewModel.setShowYearMonthPickerState(newState) }, - selectedYear = selectedYear, - selectedMonth = selectedMonth, + selectedYear = selectedYear.toLocalizedYearLabel(), + selectedMonth = selectedMonth.toLocalizedMonthLabel(), ) }, containerColor = ClodyTheme.colors.white, @@ -375,7 +377,7 @@ fun HomeScreen( } is CalendarState.Error -> { - homeViewModel.setErrorState(true, calendarState.message ?: stringResource(R.string.home_error_unknown)) + homeViewModel.setErrorState(true, calendarState.message) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/IntExtension.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/IntExtension.kt new file mode 100644 index 00000000..f8109c15 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/IntExtension.kt @@ -0,0 +1,23 @@ +package com.sopt.clody.presentation.utils.extension + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.sopt.clody.R +import kotlinx.datetime.Month +import java.time.format.TextStyle +import java.util.Locale + +@Composable +fun Int.toLocalizedYearLabel(): String = stringResource(R.string.year_format, this) + +@Composable +fun Int.toLocalizedMonthLabel(): String { + val locale = LocalContext.current.resources.configuration.locales[0] + return if (locale.language == "ko") { + stringResource(R.string.month_format, this) + } else { + Month.of(this).getDisplayName(TextStyle.FULL, Locale.ENGLISH) + .lowercase().replaceFirstChar { it.titlecase() } + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/YearMonthLabelUtil.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/YearMonthLabelUtil.kt index cbddfeb0..0e1896e2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/YearMonthLabelUtil.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/YearMonthLabelUtil.kt @@ -1,16 +1,6 @@ package com.sopt.clody.presentation.utils.extension -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.sopt.clody.R - object YearMonthLabelUtil { const val MIN_YEAR = 2000 const val MAX_YEAR = 2030 } - -@Composable -fun Int.toLocalizedYearLabel(): String = stringResource(R.string.year_format, this) - -@Composable -fun Int.toLocalizedMonthLabel(): String = stringResource(R.string.month_format, this) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 46e4a0c4..f89eb825 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -42,7 +42,9 @@ 클로버 %1$d개 - %1$d년 %2$d월 + %1$d년 + %1$d월 + %1$s %2$s 작성된 감사 일기가 없어요! 임시저장된 일기가 있어요. @@ -187,8 +189,6 @@ 오전 오후 - %1$d년 - %1$d월 데이터를 불러오는데 실패했습니다. 알 수 없는 오류가 발생했습니다. 일기 삭제 중 오류가 발생했습니다. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e61e957..86c73033 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,7 +42,9 @@ %1$d Clovers - %2$d %1$d + %1$d + %1$d + %2$s %1$s No gratitude entries yet. There are existing entries in drafts. @@ -187,8 +189,6 @@ AM PM - %1$d - %1$d An error occurred while deleting the diary. Chrome must be installed to log in. Login is not available on rooted devices for security reasons. From a61bc23929543fa714c3bcbc7574ec6c8db3c478 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 21 Jul 2025 15:18:35 +0900 Subject: [PATCH 245/299] =?UTF-8?q?[REFACTOR/#305]=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=9A=94=EC=9D=BC=20=ED=8F=AC=EB=A7=B7=EC=9D=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/calendar/component/DailyDiaryListItem.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt index f4af2913..c89ce76c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt @@ -17,6 +17,7 @@ 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.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -26,6 +27,7 @@ import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.datetime.DayOfWeek import java.time.LocalDate +import java.time.format.TextStyle @Composable fun DailyDiaryListItem( @@ -48,16 +50,13 @@ fun DailyDiaryListItem( ) { Text( text = "${date.month.value}.${date.dayOfMonth}", - style = ClodyTheme.typography.body3Medium, + style = ClodyTheme.typography.body2Medium, color = ClodyTheme.colors.gray04, modifier = Modifier.padding(vertical = 3.dp), ) Text( - text = stringResource( - id = R.string.home_daily_diary_day_of_week, - dayOfWeek.toKoreanShortLabel(), - ), - style = ClodyTheme.typography.body2Medium, + text = dayOfWeek.getDisplayName(TextStyle.FULL, LocalConfiguration.current.locales[0]), + style = ClodyTheme.typography.body2SemiBold, color = ClodyTheme.colors.gray02, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), ) From 20421175d37f484f1cb7fec45d14e5145409840d Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 21 Jul 2025 15:27:37 +0900 Subject: [PATCH 246/299] =?UTF-8?q?[REFACTOR/#305]=20=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=ED=8F=AC=EB=A7=B7=EC=9D=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/diarylist/screen/DiaryListViewModel.kt | 6 +++-- .../component/text/DiaryTitleText.kt | 22 ------------------- .../ui/writediary/screen/WriteDiaryScreen.kt | 16 +++++++++----- .../utils/extension/GetDayOfWeek.kt | 6 +++-- app/src/main/res/values-ko/strings.xml | 3 +-- app/src/main/res/values/strings.xml | 3 +-- 6 files changed, 21 insertions(+), 35 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/text/DiaryTitleText.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt index 5d5354b4..54f7e68a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.DiaryRepository -import com.sopt.clody.presentation.utils.extension.getDayOfWeek import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR @@ -12,6 +11,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import java.time.LocalDate import javax.inject.Inject @HiltViewModel @@ -74,7 +74,9 @@ class DiaryListViewModel @Inject constructor( val year = diaryDate[0].toInt() val month = diaryDate[1].toInt() val day = diaryDate[2].toInt() - val dayOfWeek = getDayOfWeek(year, month, day) + + val date = LocalDate.of(year, month, day) + val dayOfWeek = date.dayOfWeek.toString() _selectedDiaryDate.value = DiaryDate(year, month, day, dayOfWeek) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/text/DiaryTitleText.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/text/DiaryTitleText.kt deleted file mode 100644 index a1376eae..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/component/text/DiaryTitleText.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.sopt.clody.presentation.ui.writediary.component.text - -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.sopt.clody.ui.theme.ClodyTheme - -@Composable -fun DiaryTitleText(date: String, separator: String, day: String, modifier: Modifier = Modifier) { - Text( - text = "$date$separator$day", - style = ClodyTheme.typography.head2, - color = ClodyTheme.colors.gray01, - ) -} - -@Preview(showBackground = true) -@Composable -fun PreviewDiaryTitleText() { - DiaryTitleText(date = "6월 26일", separator = " / ", day = "목요일") -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt index b00eba12..da0b7fa8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -39,7 +40,6 @@ import com.sopt.clody.presentation.ui.component.dialog.FailureDialog import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage import com.sopt.clody.presentation.ui.writediary.component.bottomsheet.DeleteWriteDiaryBottomSheet import com.sopt.clody.presentation.ui.writediary.component.button.AddDiaryEntryFAB -import com.sopt.clody.presentation.ui.writediary.component.text.DiaryTitleText import com.sopt.clody.presentation.ui.writediary.component.textfield.WriteDiaryTextField import com.sopt.clody.presentation.ui.writediary.component.tooltip.TooltipIcon import com.sopt.clody.presentation.ui.writediary.component.topbar.WriteDiaryTopBar @@ -50,6 +50,7 @@ import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.LaunchedEffectWhenStarted import com.sopt.clody.presentation.utils.extension.getDayOfWeek import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage +import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -242,10 +243,15 @@ fun WriteDiaryScreen( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - DiaryTitleText( - date = stringResource(R.string.write_diary_month_and_date, month, day), - separator = " ", - day = getDayOfWeek(year, month, day), + Text( + text = stringResource( + R.string.write_diary_month_date_day_of_week, + getDayOfWeek(year, month, day), + month.toLocalizedMonthLabel(), + day, + ), + style = ClodyTheme.typography.head2, + color = ClodyTheme.colors.gray01, ) Spacer(modifier = Modifier.weight(1f)) TooltipIcon(tooltipsText = stringResource(id = R.string.write_diary_help_message)) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/GetDayOfWeek.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/GetDayOfWeek.kt index 52ceb54f..96a745b1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/GetDayOfWeek.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/GetDayOfWeek.kt @@ -1,10 +1,12 @@ package com.sopt.clody.presentation.utils.extension +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration import java.time.LocalDate import java.time.format.TextStyle -import java.util.Locale +@Composable fun getDayOfWeek(year: Int, month: Int, day: Int): String { val date = LocalDate.of(year, month, day) - return date.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.KOREAN) + return date.dayOfWeek.getDisplayName(TextStyle.FULL, LocalConfiguration.current.locales[0]) } diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f89eb825..a95f07a8 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -49,7 +49,6 @@ 작성된 감사 일기가 없어요! 임시저장된 일기가 있어요. %1$d. %2$s - %1$s요일 일기 쓰기 답장 확인 @@ -57,7 +56,7 @@ 보내기 - %1$d월 %2$d일 + %2$s %3$s일 %1$s 신조어, 비속어, 이모지 작성은 불가능해요 일상 속 작은 감사함을 적어보세요 2~50자 까지 입력할 수 있어요. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 86c73033..6d3cd750 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,7 +49,6 @@ No gratitude entries yet. There are existing entries in drafts. %1$d. %2$s - %1$s Write a Journal See my Reply @@ -57,7 +56,7 @@ Send - %1$d %2$d + %1$s, %2$s %3$s Please avoid using slang, profanity, or emojis. Something you\'re grateful for today. Please enter between 2 and 100 characters. From 8398df3136efe7eeb1392dcad2ad452e14606549 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 21 Jul 2025 15:33:41 +0900 Subject: [PATCH 247/299] =?UTF-8?q?[REFACTOR/#305]=20=EB=8B=B5=EC=9E=A5?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=EB=82=A0=EC=A7=9C=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/ui/replydiary/ReplyDiaryScreen.kt | 6 +++--- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt index 65efb8b1..c391ff08 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt @@ -41,6 +41,7 @@ import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage +import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -118,7 +119,7 @@ fun ReplyDiaryScreen( modifier = Modifier.statusBarsPadding(), title = { Text( - text = stringResource(R.string.reply_month_and_date, month, date), + text = stringResource(R.string.reply_month_and_date, month.toLocalizedMonthLabel(), date.toString()), style = ClodyTheme.typography.head4, color = ClodyTheme.colors.gray01, ) @@ -137,13 +138,12 @@ fun ReplyDiaryScreen( content = { innerPadding -> Box( modifier = Modifier - .fillMaxSize() .background(ClodyTheme.colors.white) .padding(innerPadding), ) { Column( modifier = Modifier - .fillMaxWidth() + .fillMaxSize() .padding(horizontal = 24.dp) .padding(bottom = 28.dp) .clip(RoundedCornerShape(16.dp)) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index a95f07a8..03d82c2a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -70,7 +70,7 @@ 열어보기 - %1$d월 %2$d일 + %1$s %2$s일 %1$s님을 위한 행운의 답장 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6d3cd750..eb3c0672 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,8 +70,8 @@ Open Reply - %1$d %2$d - A lucky reply for %1$d + %1$s %2$s + A lucky reply for %1$s %2$s %1$s From 3cffbb29fd09308ff306de2f1dba1a2a553838fa Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 21 Jul 2025 15:35:53 +0900 Subject: [PATCH 248/299] =?UTF-8?q?[REFACTOR/#305]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=ED=8F=AC=EB=A7=B7=EC=9D=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/utils/extension/StringExt.kt | 11 ++++++++--- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt index 6eeb8e26..ff32b0d7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt @@ -1,9 +1,14 @@ package com.sopt.clody.presentation.utils.extension +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.sopt.clody.R + +@Composable fun String.convertTo12HourFormat(): String { val (hourBefore, minuteBefore) = this.split(":").map { it.toInt() } - val amPm = if (hourBefore < 12) "오전" else "오후" + val amPm = if (hourBefore < 12) stringResource(R.string.time_am) else stringResource(R.string.time_pm) val hourAfter = when { hourBefore == 0 -> 12 @@ -11,9 +16,9 @@ fun String.convertTo12HourFormat(): String { else -> hourBefore } - val minuteAfter = if (minuteBefore == 0) "00" else minuteBefore + val minuteAfter = if (minuteBefore == 0) "00" else minuteBefore.toString() - return String.format("$amPm ${hourAfter}시 ${minuteAfter}분") + return String.format(stringResource(R.string.notification_setting_selected_time, amPm, hourAfter, minuteAfter)) } fun Triple.to24HourFormat(): String { diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 03d82c2a..f8ab3668 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -104,7 +104,7 @@ 일기 작성 알림 받기 이어쓰기 알림 받기 알림 시간 - %1$s %2$s시 %3$s분 + %1$s %2$d시 %3$s분 답장 도착 알림 받기 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb3c0672..053c2cfc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,7 +104,7 @@ Journal Reminder Draft Reminder Reminder Time - %2$s:%3$s %1$s + %2$d:%3$s %1$s Reply Notification From 82c98719eefbdbfeb82be4f0f9ec6ac29d32f241 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 21 Jul 2025 15:37:10 +0900 Subject: [PATCH 249/299] =?UTF-8?q?[REFACTOR/#305]=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=EB=AA=A8=EC=95=84=EB=B3=B4=EA=B8=B0=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=ED=8F=AC=EB=A7=B7=EC=9D=84=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/diarylist/component/DiaryListTopAppBar.kt | 4 ++-- .../presentation/ui/diarylist/screen/DiaryListScreen.kt | 6 ++++-- app/src/main/res/values-ko/strings.xml | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DiaryListTopAppBar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DiaryListTopAppBar.kt index a2b0d8ae..406866b5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DiaryListTopAppBar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DiaryListTopAppBar.kt @@ -29,8 +29,8 @@ import com.sopt.clody.ui.theme.ClodyTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun DiaryListTopAppBar( - selectedYear: Int, - selectedMonth: Int, + selectedYear: String, + selectedMonth: String, showYearMonthPicker: () -> Unit, onClickCalendar: () -> Unit, ) { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt index cc86c8f9..ae5e732a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt @@ -25,6 +25,8 @@ import com.sopt.clody.presentation.ui.diarylist.component.EmptyDiaryList import com.sopt.clody.presentation.ui.diarylist.component.MonthlyDiaryList import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel +import com.sopt.clody.presentation.utils.extension.toLocalizedYearLabel import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme @@ -130,8 +132,8 @@ fun DiaryListScreen( Scaffold( topBar = { DiaryListTopAppBar( - selectedYear = selectedYearInDiaryList, - selectedMonth = selectedMonthInDiaryList, + selectedYear = selectedYearInDiaryList.toLocalizedYearLabel(), + selectedMonth = selectedMonthInDiaryList.toLocalizedMonthLabel(), showYearMonthPicker = showYearMonthPicker, onClickCalendar = onClickCalendar, ) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f8ab3668..daad26b3 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -74,7 +74,7 @@ %1$s님을 위한 행운의 답장 - %1$s년 %2$s월 + %1$s %2$s 작성된 감사일기가 없어요 %1$s일 /%1$s From dc1d9c1611313a64733f04b3313d4fe9a2ff528d Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 21 Jul 2025 15:38:21 +0900 Subject: [PATCH 250/299] =?UTF-8?q?[REFACTOR/#305]=20=ED=99=88,=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=99=94=EB=A9=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20=EC=97=B0?= =?UTF-8?q?=EC=9B=94=ED=94=BC=EC=BB=A4=EB=A5=BC=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20=EC=98=81=EC=96=B4=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=EC=9D=80=20=EC=9B=94->=EC=97=B0=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C,=20=ED=95=9C=EA=B5=AD=EC=96=B4=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=9D=80=20=EC=97=B0->=EC=9B=94=20=EC=88=9C=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/timepicker/YearMonthPicker.kt | 169 +++++++++++++++--- .../timepicker/YearMonthPickerItem.kt | 123 ------------- 2 files changed, 147 insertions(+), 145 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPickerItem.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt index 181cc474..f84c68f5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt @@ -2,6 +2,8 @@ package com.sopt.clody.presentation.ui.component.timepicker import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectVerticalDragGestures +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -14,16 +16,29 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.button.ClodyButton @@ -31,6 +46,9 @@ import com.sopt.clody.presentation.utils.extension.YearMonthLabelUtil import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel import com.sopt.clody.presentation.utils.extension.toLocalizedYearLabel import com.sopt.clody.ui.theme.ClodyTheme +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import java.util.Locale @Composable fun YearMonthPicker( @@ -42,6 +60,9 @@ fun YearMonthPicker( val yearItems = remember { (YearMonthLabelUtil.MIN_YEAR..YearMonthLabelUtil.MAX_YEAR).toList() } val monthItems = remember { (1..12).toList() } + val yearLabelItems = yearItems.map { it.toLocalizedYearLabel() } + val monthLabelItems = monthItems.map { it.toLocalizedMonthLabel() } + val yearPickerState = rememberPickerState() val monthPickerState = rememberPickerState() @@ -88,9 +109,6 @@ fun YearMonthPicker( } } - val yearLabelItems = yearItems.map { it.toLocalizedYearLabel() } - val monthLabelItems = monthItems.map { it.toLocalizedMonthLabel() } - Box(modifier = Modifier.fillMaxWidth()) { Box( modifier = Modifier @@ -104,28 +122,51 @@ fun YearMonthPicker( verticalAlignment = Alignment.CenterVertically, ) { Spacer(modifier = Modifier.weight(1f)) - YearMonthPickerItem( - state = yearPickerState, - items = yearLabelItems, - startIndex = startYearIndex, - visibleItemsCount = 5, - infiniteScroll = false, - modifier = Modifier.weight(1f), - textModifier = Modifier.padding(8.dp), - ) - Spacer(modifier = Modifier.width(20.dp)) - YearMonthPickerItem( - state = monthPickerState, - items = monthLabelItems, - startIndex = startMonthIndex, - visibleItemsCount = 5, - infiniteScroll = false, - modifier = Modifier.weight(1f), - textModifier = Modifier.padding(8.dp), - ) + if (LocalConfiguration.current.locales[0] == Locale.KOREA) { + YearMonthPickerItem( + state = yearPickerState, + items = yearLabelItems, + startIndex = startYearIndex, + visibleItemsCount = 5, + infiniteScroll = false, + modifier = Modifier.weight(2f), + textModifier = Modifier.padding(8.dp), + ) + Spacer(modifier = Modifier.width(20.dp)) + YearMonthPickerItem( + state = monthPickerState, + items = monthLabelItems, + startIndex = startMonthIndex, + visibleItemsCount = 5, + infiniteScroll = false, + modifier = Modifier.weight(2f), + textModifier = Modifier.padding(8.dp), + ) + } else { + YearMonthPickerItem( + state = monthPickerState, + items = monthLabelItems, + startIndex = startMonthIndex, + visibleItemsCount = 5, + infiniteScroll = false, + modifier = Modifier.weight(2f), + textModifier = Modifier.padding(8.dp), + ) + Spacer(modifier = Modifier.width(20.dp)) + YearMonthPickerItem( + state = yearPickerState, + items = yearLabelItems, + startIndex = startYearIndex, + visibleItemsCount = 5, + infiniteScroll = false, + modifier = Modifier.weight(2f), + textModifier = Modifier.padding(8.dp), + ) + } Spacer(modifier = Modifier.weight(1f)) } } + ClodyButton( onClick = { val year = yearItems[yearLabelItems.indexOf(yearPickerState.selectedItem)] @@ -142,3 +183,87 @@ fun YearMonthPicker( } } } + +@Composable +fun YearMonthPickerItem( + modifier: Modifier = Modifier, + items: List, + state: PickerState = rememberPickerState(), + startIndex: Int = 0, + visibleItemsCount: Int, + textModifier: Modifier = Modifier, + infiniteScroll: Boolean = true, +) { + val visibleItemsMiddle = visibleItemsCount / 2 + val emptyItems = List(visibleItemsMiddle) { "" } + val paddedItems = emptyItems + items + emptyItems + val listScrollCount = if (infiniteScroll) Integer.MAX_VALUE else paddedItems.size + val listScrollMiddle = listScrollCount / 2 + val listStartIndex = if (infiniteScroll) { + listScrollMiddle - listScrollMiddle % paddedItems.size - visibleItemsMiddle + startIndex + } else { + startIndex + visibleItemsMiddle + } + + fun getItem(index: Int) = paddedItems.getOrNull(index).orEmpty() + + val listState = rememberLazyListState(initialFirstVisibleItemIndex = listStartIndex) + val flingBehavior = rememberSnapFlingBehavior(lazyListState = listState) + + val itemHeightPixels = remember { mutableIntStateOf(0) } + val itemHeightDp = with(LocalDensity.current) { itemHeightPixels.intValue.toDp() } + + val fadingEdgeGradient = remember { + Brush.verticalGradient( + 0f to Color.White.copy(alpha = 0.9f), + 0.1f to Color.White.copy(alpha = 0.8f), + 0.2f to Color.White.copy(alpha = 0.7f), + 0.3f to Color.White.copy(alpha = 0.6f), + 0.4f to Color.Transparent, + 0.5f to Color.Transparent, + 0.6f to Color.Transparent, + 0.7f to Color.White.copy(alpha = 0.7f), + 0.8f to Color.White.copy(alpha = 0.8f), + 0.9f to Color.White.copy(alpha = 0.9f), + ) + } + + LaunchedEffect(listState) { + snapshotFlow { listState.firstVisibleItemIndex } + .map { index -> getItem(index + visibleItemsMiddle) } + .distinctUntilChanged() + .collect { item -> state.selectedItem = item } + } + + Box(modifier = modifier) { + LazyColumn( + state = listState, + flingBehavior = flingBehavior, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .height(itemHeightDp * visibleItemsCount) + .pointerInput(Unit) { + detectVerticalDragGestures { change, dragAmount -> + change.consume() + } + } + .drawWithContent { + drawContent() + drawRect(fadingEdgeGradient, size = size) + }, + ) { + items(listScrollCount) { index -> + Text( + text = getItem(index), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = ClodyTheme.typography.head3Medium.copy(color = ClodyTheme.colors.gray01), + modifier = Modifier + .onSizeChanged { size -> itemHeightPixels.intValue = size.height } + .then(textModifier), + ) + } + } + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPickerItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPickerItem.kt deleted file mode 100644 index 813f6ac7..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPickerItem.kt +++ /dev/null @@ -1,123 +0,0 @@ -package com.sopt.clody.presentation.ui.component.timepicker - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.detectVerticalDragGestures -import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import com.sopt.clody.ui.theme.ClodyTheme -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun YearMonthPickerItem( - modifier: Modifier = Modifier, - items: List, - state: PickerState = rememberPickerState(), - startIndex: Int = 0, - visibleItemsCount: Int, - textModifier: Modifier = Modifier, - infiniteScroll: Boolean = true, -) { - val visibleItemsMiddle = visibleItemsCount / 2 - val emptyItems = List(visibleItemsMiddle) { "" } - val paddedItems = emptyItems + items + emptyItems - val listScrollCount = if (infiniteScroll) Integer.MAX_VALUE else paddedItems.size - val listScrollMiddle = listScrollCount / 2 - val listStartIndex = if (infiniteScroll) { - listScrollMiddle - listScrollMiddle % paddedItems.size - visibleItemsMiddle + startIndex - } else { - startIndex + visibleItemsMiddle - } - - fun getItem(index: Int) = paddedItems.getOrNull(index).orEmpty() - - val listState = rememberLazyListState(initialFirstVisibleItemIndex = listStartIndex) - val flingBehavior = rememberSnapFlingBehavior(lazyListState = listState) - - val itemHeightPixels = remember { mutableIntStateOf(0) } - val itemHeightDp = with(LocalDensity.current) { itemHeightPixels.intValue.toDp() } - - val fadingEdgeGradient = remember { - Brush.verticalGradient( - 0f to Color.White.copy(alpha = 0.9f), - 0.1f to Color.White.copy(alpha = 0.8f), - 0.2f to Color.White.copy(alpha = 0.7f), - 0.3f to Color.White.copy(alpha = 0.6f), - 0.4f to Color.Transparent, - 0.5f to Color.Transparent, - 0.6f to Color.Transparent, - 0.7f to Color.White.copy(alpha = 0.7f), - 0.8f to Color.White.copy(alpha = 0.8f), - 0.9f to Color.White.copy(alpha = 0.9f), - ) - } - - LaunchedEffect(listState) { - snapshotFlow { listState.firstVisibleItemIndex } - .map { index -> getItem(index + visibleItemsMiddle) } - .distinctUntilChanged() - .collect { item -> state.selectedItem = item } - } - - Box(modifier = modifier) { - LazyColumn( - state = listState, - flingBehavior = flingBehavior, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .height(itemHeightDp * visibleItemsCount) - .pointerInput(Unit) { - detectVerticalDragGestures { change, dragAmount -> - change.consume() - } - } - .drawWithContent { - drawContent() - drawRect(fadingEdgeGradient, size = size) - }, - ) { - items(listScrollCount) { index -> - Text( - text = getItem(index), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = ClodyTheme.typography.head3Medium.copy(color = ClodyTheme.colors.gray01), - modifier = Modifier - .onSizeChanged { size -> itemHeightPixels.intValue = size.height } - .then(textModifier), - ) - } - } - } -} - -@Preview(showBackground = true) -@Composable -fun PreviewPicker() { - YearMonthPickerItem( - items = (1..99).map { it.toString() }, - visibleItemsCount = 5, - ) -} From 384a6b1d61888797179803b7e4f17ffa9ed94b81 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 21 Jul 2025 15:38:43 +0900 Subject: [PATCH 251/299] =?UTF-8?q?[REFACTOR/#305]=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=8B=AC=EB=A0=A5=EC=9D=98=20=EC=9A=94=EC=9D=BC=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/calendar/component/WeekHeader.kt | 58 ++----------------- 1 file changed, 4 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt index 9751c3fb..41e86052 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt @@ -11,54 +11,18 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.datetime.DayOfWeek - -enum class WeekLang { - KOREAN, ENGLISH -} - -fun DayOfWeek.toKoreanShortLabel(): String { - return when (this) { - DayOfWeek.SUNDAY -> "일" - DayOfWeek.MONDAY -> "월" - DayOfWeek.TUESDAY -> "화" - DayOfWeek.WEDNESDAY -> "수" - DayOfWeek.THURSDAY -> "목" - DayOfWeek.FRIDAY -> "금" - DayOfWeek.SATURDAY -> "토" - } -} - -fun DayOfWeek.toEnglishShortLabel(): String { - return when (this) { - DayOfWeek.SUNDAY -> "Sun" - DayOfWeek.MONDAY -> "Mon" - DayOfWeek.TUESDAY -> "Tue" - DayOfWeek.WEDNESDAY -> "Wed" - DayOfWeek.THURSDAY -> "Thu" - DayOfWeek.FRIDAY -> "Fri" - DayOfWeek.SATURDAY -> "Sat" - } -} - -fun DayOfWeek.getLabel(lang: WeekLang): String { - return when (lang) { - WeekLang.KOREAN -> this.toKoreanShortLabel() - WeekLang.ENGLISH -> this.toEnglishShortLabel() - } -} +import java.time.format.TextStyle @Composable fun WeekHeader( modifier: Modifier = Modifier, itemWidth: Dp = (LocalConfiguration.current.screenWidthDp.dp - 40.dp) / 7, - lang: WeekLang = WeekLang.KOREAN, ) { - val weekLabelArray = listOf( + val dayOfWeekArray = listOf( DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.TUESDAY, @@ -68,19 +32,17 @@ fun WeekHeader( DayOfWeek.SATURDAY, ) - val labels = weekLabelArray.map { it.getLabel(lang) } - Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier.fillMaxWidth(), ) { - labels.forEach { week -> + dayOfWeekArray.forEach { week -> Box( modifier = Modifier.width(itemWidth), contentAlignment = Alignment.Center, ) { Text( - text = week, + text = week.getDisplayName(TextStyle.NARROW, LocalConfiguration.current.locales[0]), color = ClodyTheme.colors.gray05, style = ClodyTheme.typography.detail1Medium, textAlign = TextAlign.Center, @@ -89,15 +51,3 @@ fun WeekHeader( } } } - -@Preview(showBackground = true) -@Composable -fun WeekHeaderKoreanPreview() { - WeekHeader(lang = WeekLang.KOREAN) -} - -@Preview(showBackground = true) -@Composable -fun WeekHeaderEnglishPreview() { - WeekHeader(lang = WeekLang.ENGLISH) -} From dd449f1c64c794fd272cbc04dad0326d50f4619e Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 21 Jul 2025 15:40:14 +0900 Subject: [PATCH 252/299] =?UTF-8?q?[FIX/#305]=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=9D=B4=20=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EB=93=A4=EC=9D=84=20=EB=B0=98=EC=98=81=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20-=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=A4=91=EC=95=99=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20-=20=EC=A0=95=EA=B7=9C=EC=8B=9D=20=EB=AC=B8?= =?UTF-8?q?=EB=B2=95=20=EC=88=98=EC=A0=95=20-=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EB=A6=AC=EC=86=8C=EC=8A=A4=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 --- .../sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt | 2 +- .../sopt/clody/presentation/ui/component/dialog/ClodyDialog.kt | 1 + .../presentation/ui/writediary/screen/WriteDiaryViewModel.kt | 2 +- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index 56c63c60..0909bcd7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -175,7 +175,7 @@ class SignUpViewModel @AssistedInject constructor( private fun validateNickname(nickname: String): Boolean { val state = withState(this@SignUpViewModel) { it } setState { copy(nicknameMaxLength = languageProvider.getNicknameMaxLength()) } - val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,${state.nicknameMaxLength}$".toRegex() + val regex = "^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,${state.nicknameMaxLength}}$".toRegex() return nickname.matches(regex) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/ClodyDialog.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/ClodyDialog.kt index c0846c94..cdd8265d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/ClodyDialog.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/dialog/ClodyDialog.kt @@ -77,6 +77,7 @@ fun ClodyDialog( Text( text = titleMassage, + textAlign = TextAlign.Center, style = ClodyTheme.typography.body1SemiBold, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index fc19175d..61c133cc 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -159,7 +159,7 @@ class WriteDiaryViewModel @Inject constructor( private fun isValidEntry(text: String): Boolean { val textWithoutSpaces = text.replace("\\s".toRegex(), "") - return textWithoutSpaces.matches(Regex("^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣\\W]{2,${_diaryMaxLength.value}$")) + return textWithoutSpaces.matches(Regex("^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣\\W]{2,${_diaryMaxLength.value}}$")) } private fun checkLimitMessage() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 053c2cfc..ee023ea5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -167,7 +167,7 @@ Change reminder time Save - Heads up- drafts expire\\nDon\'t miss Lody\'s reply! + Heads up- drafts expire\nDon\'t miss Lody\'s reply! To receive a reminder before the reply deadline,\nconfirm notification permissions. [Settings > Apps > Clody > Notifications] Turn on Notifications From 81e225d2318db724bc548a513d96d4788e48ec05 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 23 Jul 2025 16:20:59 +0900 Subject: [PATCH 253/299] =?UTF-8?q?[MOD/#305]=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=97=90=20=EC=A0=81=ED=95=A9=ED=95=9C=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/extension/{IntExtension.kt => DateLabelExtension.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/sopt/clody/presentation/utils/extension/{IntExtension.kt => DateLabelExtension.kt} (100%) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/IntExtension.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/DateLabelExtension.kt similarity index 100% rename from app/src/main/java/com/sopt/clody/presentation/utils/extension/IntExtension.kt rename to app/src/main/java/com/sopt/clody/presentation/utils/extension/DateLabelExtension.kt From a80bed0173f1a1eb03d6f394b7e5722ee8b5ccf6 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 23 Jul 2025 19:59:45 +0900 Subject: [PATCH 254/299] =?UTF-8?q?[REFACTOR/#305]=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EB=B9=97=20=EB=A6=AC=EB=B7=B0=EB=A5=BC=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20=EB=A1=9C=EC=BC=80?= =?UTF-8?q?=EC=9D=BC=EC=9D=84=20=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8A=94=20?= =?UTF-8?q?=EA=B3=BC=EC=A0=95=EC=97=90=EC=84=9C=20=EC=95=88=EC=A0=95?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=94=94=ED=8F=B4?= =?UTF-8?q?=ED=8A=B8=EA=B0=92=EC=9D=84=20=EC=84=A4=EC=A0=95=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20-=20YearMonthPicker=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=EC=9D=84=20=EC=A1=B0=EC=A0=95=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/component/timepicker/YearMonthPicker.kt | 16 ++++++++-------- .../ui/diarylist/screen/DiaryListViewModel.kt | 4 +++- .../calendar/component/DailyDiaryListItem.kt | 5 ++++- .../utils/extension/DateLabelExtension.kt | 4 ++-- .../presentation/utils/extension/GetDayOfWeek.kt | 3 ++- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt index f84c68f5..e91a665e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/component/timepicker/YearMonthPicker.kt @@ -121,49 +121,49 @@ fun YearMonthPicker( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - Spacer(modifier = Modifier.weight(1f)) if (LocalConfiguration.current.locales[0] == Locale.KOREA) { + Spacer(modifier = Modifier.width(20.dp)) YearMonthPickerItem( state = yearPickerState, items = yearLabelItems, startIndex = startYearIndex, visibleItemsCount = 5, infiniteScroll = false, - modifier = Modifier.weight(2f), + modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), ) - Spacer(modifier = Modifier.width(20.dp)) YearMonthPickerItem( state = monthPickerState, items = monthLabelItems, startIndex = startMonthIndex, visibleItemsCount = 5, infiniteScroll = false, - modifier = Modifier.weight(2f), + modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), ) + Spacer(modifier = Modifier.width(20.dp)) } else { + Spacer(modifier = Modifier.width(20.dp)) YearMonthPickerItem( state = monthPickerState, items = monthLabelItems, startIndex = startMonthIndex, visibleItemsCount = 5, infiniteScroll = false, - modifier = Modifier.weight(2f), + modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), ) - Spacer(modifier = Modifier.width(20.dp)) YearMonthPickerItem( state = yearPickerState, items = yearLabelItems, startIndex = startYearIndex, visibleItemsCount = 5, infiniteScroll = false, - modifier = Modifier.weight(2f), + modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), ) + Spacer(modifier = Modifier.width(20.dp)) } - Spacer(modifier = Modifier.weight(1f)) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt index 54f7e68a..f3ea9ae2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt @@ -12,6 +12,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import java.time.LocalDate +import java.time.format.TextStyle +import java.util.Locale import javax.inject.Inject @HiltViewModel @@ -76,7 +78,7 @@ class DiaryListViewModel @Inject constructor( val day = diaryDate[2].toInt() val date = LocalDate.of(year, month, day) - val dayOfWeek = date.dayOfWeek.toString() + val dayOfWeek = date.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.getDefault()) _selectedDiaryDate.value = DiaryDate(year, month, day, dayOfWeek) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt index c89ce76c..7268a3aa 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt @@ -55,7 +55,10 @@ fun DailyDiaryListItem( modifier = Modifier.padding(vertical = 3.dp), ) Text( - text = dayOfWeek.getDisplayName(TextStyle.FULL, LocalConfiguration.current.locales[0]), + text = dayOfWeek.getDisplayName( + TextStyle.FULL, + LocalConfiguration.current.locales.let { if (it.isEmpty) java.util.Locale.getDefault() else it[0] }, + ), style = ClodyTheme.typography.body2SemiBold, color = ClodyTheme.colors.gray02, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/DateLabelExtension.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/DateLabelExtension.kt index f8109c15..f326f70c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/DateLabelExtension.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/DateLabelExtension.kt @@ -13,11 +13,11 @@ fun Int.toLocalizedYearLabel(): String = stringResource(R.string.year_format, th @Composable fun Int.toLocalizedMonthLabel(): String { - val locale = LocalContext.current.resources.configuration.locales[0] + val locales = LocalContext.current.resources.configuration.locales + val locale = if (locales.isEmpty) Locale.getDefault() else locales[0] return if (locale.language == "ko") { stringResource(R.string.month_format, this) } else { Month.of(this).getDisplayName(TextStyle.FULL, Locale.ENGLISH) - .lowercase().replaceFirstChar { it.titlecase() } } } diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/GetDayOfWeek.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/GetDayOfWeek.kt index 96a745b1..d07a5ab8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/GetDayOfWeek.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/GetDayOfWeek.kt @@ -8,5 +8,6 @@ import java.time.format.TextStyle @Composable fun getDayOfWeek(year: Int, month: Int, day: Int): String { val date = LocalDate.of(year, month, day) - return date.dayOfWeek.getDisplayName(TextStyle.FULL, LocalConfiguration.current.locales[0]) + val locale = LocalConfiguration.current.locales.let { if (it.isEmpty) java.util.Locale.getDefault() else it[0] } + return date.dayOfWeek.getDisplayName(TextStyle.FULL, locale) } From 4b8e4b3400a2c749b5e1c1db165db6608ae2230d Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 23 Jul 2025 23:02:22 +0900 Subject: [PATCH 255/299] =?UTF-8?q?[ADD/#292]=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DataStore Preferences 라이브러리 의존성 추가 --- app/build.gradle.kts | 6 ++++++ gradle/libs.versions.toml | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b382f787..c8bfb09f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,12 +31,14 @@ android { val amplitudeApiKey: String = properties.getProperty("amplitude.api.key") val googleAdmobAppId: String = properties.getProperty("GOOGLE_ADMOB_APP_ID", "") val googleAdmobUnitId: String = properties.getProperty("GOOGLE_ADMOB_UNIT_ID", "") + val googleAuthWebClientId: String = properties.getProperty("GOOGLE_AUTH_WEB_CLIENT_ID", "") val allowedDomains: String = properties.getProperty("allowed.webview.domains", "notion.so,google.com") buildConfigField("String", "GOOGLE_ADMOB_APP_ID", "\"$googleAdmobAppId\"") buildConfigField("String", "GOOGLE_ADMOB_UNIT_ID", "\"$googleAdmobUnitId\"") buildConfigField("String", "KAKAO_API_KEY", "\"$kakaoApiKey\"") buildConfigField("String", "AMPLITUDE_API_KEY", "\"$amplitudeApiKey\"") + buildConfigField("String", "GOOGLE_AUTH_WEB_CLIENT_ID", "\"$googleAuthWebClientId\"") buildConfigField("String", "ALLOWED_WEBVIEW_DOMAINS", "\"$allowedDomains\"") manifestPlaceholders["kakaoRedirectUri"] = "kakao$kakaoApiKey" manifestPlaceholders["GOOGLE_ADMOB_APP_ID"] = googleAdmobAppId @@ -153,4 +155,8 @@ dependencies { implementation(libs.coil) implementation(libs.kakao.user) implementation(libs.kotlinx.datetime) + + implementation(libs.androidx.credentials.play.services.auth) + implementation(libs.google.auth) + implementation(libs.androidx.datastore.preferences) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1e3c001a..a95a7d14 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,6 +53,10 @@ mockk = "1.13.10" play-review = "2.0.2" +androidxCredentials = "1.5.0" +googleAuth = "21.3.0" +datastore = "1.1.7" + [libraries] # AndroidX Core core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } @@ -136,6 +140,10 @@ mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } play-review = { group = "com.google.android.play", name = "review", version.ref = "play-review"} play-review-ktx = { group = "com.google.android.play", name = "review-ktx", version.ref = "play-review"} +androidx-credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "androidxCredentials" } +google-auth = { module = "com.google.android.gms:play-services-auth", version.ref = "googleAuth" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } From 87d03d7a35b35ce1d6cbf2f341a4403b4923aff7 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 23 Jul 2025 23:02:52 +0900 Subject: [PATCH 256/299] =?UTF-8?q?[REFACTOR/#292]=20=EC=8A=A4=ED=94=8C?= =?UTF-8?q?=EB=9E=98=EC=8B=9C=20=ED=99=94=EB=A9=B4=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20=ED=99=95=EC=9D=B8=20=ED=83=90=EC=83=89=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `attemptAutoLogin` 메서드가 로그인 상태를 반환하도록 수정했습니다. --- .../presentation/ui/splash/SplashViewModel.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt index 5d3deac1..f17f5988 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt @@ -4,7 +4,6 @@ import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory -import com.airbnb.mvrx.withState import com.sopt.clody.BuildConfig import com.sopt.clody.domain.appupdate.AppUpdateChecker import com.sopt.clody.domain.model.AppUpdateState @@ -58,8 +57,9 @@ class SplashViewModel @AssistedInject constructor( AmplitudeUtils.trackEvent(AmplitudeConstraints.ALARM) } if (!BuildConfig.DEBUG && checkInspectionAndHandle()) return - checkVersionAndNavigate() - attemptAutoLogin() + + val isLoggedIn = attemptAutoLogin() + checkVersionAndNavigate(isLoggedIn) } private suspend fun checkInspectionAndHandle(): Boolean { @@ -76,21 +76,20 @@ class SplashViewModel @AssistedInject constructor( return false } - private fun attemptAutoLogin() { + private fun attemptAutoLogin(): Boolean { val isLoggedIn = tokenRepository.getAccessToken().isNotBlank() && tokenRepository.getRefreshToken().isNotBlank() setState { copy(isUserLoggedIn = isLoggedIn) } + return isLoggedIn } - private suspend fun checkVersionAndNavigate() { + private suspend fun checkVersionAndNavigate(isLoggedIn: Boolean) { val updateState = appUpdateChecker.getAppUpdateState(BuildConfig.VERSION_NAME) setState { copy(updateState = updateState) } if (updateState == AppUpdateState.Latest) { delay(1000) - val isLoggedIn = withState(this@SplashViewModel) { it.isUserLoggedIn } - - if (isLoggedIn == true) { + if (isLoggedIn) { _sideEffects.send(SplashContract.SplashSideEffect.NavigateToHome) } else { _sideEffects.send(SplashContract.SplashSideEffect.NavigateToLogin) From e8c32c6f63c99a488c9ec9ff1af952cac0e35bc5 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 23 Jul 2025 23:03:34 +0900 Subject: [PATCH 257/299] =?UTF-8?q?[ADD/#292]=20OAuth=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OAuthDataStore 추가 - ID 토큰 및 플랫폼 정보 저장, 조회, 삭제 기능 구현 - DataStore를 사용하여 OAuth 관련 정보 관리 - OAuthDataStoreKeys 추가 - DataStore 키 정의 --- .../clody/data/datastore/DataStoreModule.kt | 22 +++++++++++ .../clody/data/datastore/OAuthDataStore.kt | 39 +++++++++++++++++++ .../data/datastore/OAuthDataStoreKeys.kt | 8 ++++ 3 files changed, 69 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/data/datastore/DataStoreModule.kt create mode 100644 app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt create mode 100644 app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStoreKeys.kt diff --git a/app/src/main/java/com/sopt/clody/data/datastore/DataStoreModule.kt b/app/src/main/java/com/sopt/clody/data/datastore/DataStoreModule.kt new file mode 100644 index 00000000..68f1472c --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/datastore/DataStoreModule.kt @@ -0,0 +1,22 @@ +package com.sopt.clody.data.datastore + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataStoreModule { + + @Provides + @Singleton + fun provideOAuthDataStore( + @ApplicationContext context: Context, + ): OAuthDataStore { + return OAuthDataStore(context) + } +} diff --git a/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt b/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt new file mode 100644 index 00000000..443cc227 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt @@ -0,0 +1,39 @@ +package com.sopt.clody.data.datastore + +import android.content.Context +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import com.sopt.clody.presentation.ui.login.OAuthProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +class OAuthDataStore @Inject constructor(@ApplicationContext context: Context) { + private val Context.dataStore by preferencesDataStore(name = "oauth_pref") + private val dataStore = context.dataStore + + suspend fun saveIdToken(token: String) { + dataStore.edit { it[OAuthDataStoreKeys.GOOGLE_ID_TOKEN] = token } + } + + suspend fun getIdToken(): String? { + return dataStore.data.first()[OAuthDataStoreKeys.GOOGLE_ID_TOKEN] + } + + suspend fun savePlatform(provider: OAuthProvider) { + dataStore.edit { it[OAuthDataStoreKeys.OAUTH_PLATFORM] = provider.name } + } + + suspend fun getPlatform(): OAuthProvider? { + return dataStore.data.first()[OAuthDataStoreKeys.OAUTH_PLATFORM]?.let { + runCatching { OAuthProvider.valueOf(it) }.getOrNull() + } + } + + suspend fun clear() { + dataStore.edit { + it.remove(OAuthDataStoreKeys.GOOGLE_ID_TOKEN) + it.remove(OAuthDataStoreKeys.OAUTH_PLATFORM) + } + } +} diff --git a/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStoreKeys.kt b/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStoreKeys.kt new file mode 100644 index 00000000..9bdac71b --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStoreKeys.kt @@ -0,0 +1,8 @@ +package com.sopt.clody.data.datastore + +import androidx.datastore.preferences.core.stringPreferencesKey + +object OAuthDataStoreKeys { + val GOOGLE_ID_TOKEN = stringPreferencesKey("google_id_token") + val OAUTH_PLATFORM = stringPreferencesKey("oauth_platform") +} From 196334a6cab89794217456fd508a90980257238b Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 23 Jul 2025 23:04:20 +0900 Subject: [PATCH 258/299] =?UTF-8?q?[FEAT/#292]=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthService에 구글 회원가입 API 함수 추가 - AuthInterceptor에 구글 회원가입 경로 추가 (헤더 미포함 처리) - GoogleSignUpRequestDto 추가 - AuthDataSource 및 AuthDataSourceImpl에 구글 회원가입 관련 함수 추가 --- .../com/sopt/clody/data/remote/api/AuthService.kt | 6 ++++++ .../clody/data/remote/datasource/AuthDataSource.kt | 2 ++ .../data/remote/datasourceimpl/AuthDataSourceImpl.kt | 4 ++++ .../remote/dto/request/GoogleSignUpRequestDto.kt | 12 ++++++++++++ .../sopt/clody/data/remote/util/AuthInterceptor.kt | 1 + 5 files changed, 25 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/data/remote/dto/request/GoogleSignUpRequestDto.kt diff --git a/app/src/main/java/com/sopt/clody/data/remote/api/AuthService.kt b/app/src/main/java/com/sopt/clody/data/remote/api/AuthService.kt index af1ae417..7229df5a 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/api/AuthService.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/api/AuthService.kt @@ -1,6 +1,7 @@ package com.sopt.clody.data.remote.api import com.sopt.clody.data.remote.dto.base.ApiResponse +import com.sopt.clody.data.remote.dto.request.GoogleSignUpRequestDto import com.sopt.clody.data.remote.dto.request.LoginRequestDto import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.dto.response.LoginResponseDto @@ -21,4 +22,9 @@ interface AuthService { @Header("Authorization") authorization: String, @Body signUpRequestDto: SignUpRequestDto, ): ApiResponse + + @POST("api/v1/auth/oauth2/google") + suspend fun signUpWithGoogle( + @Body body: GoogleSignUpRequestDto, + ): ApiResponse } diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasource/AuthDataSource.kt b/app/src/main/java/com/sopt/clody/data/remote/datasource/AuthDataSource.kt index e4dfe411..d3f03fc1 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasource/AuthDataSource.kt @@ -1,6 +1,7 @@ package com.sopt.clody.data.remote.datasource import com.sopt.clody.data.remote.dto.base.ApiResponse +import com.sopt.clody.data.remote.dto.request.GoogleSignUpRequestDto import com.sopt.clody.data.remote.dto.request.LoginRequestDto import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.dto.response.LoginResponseDto @@ -9,4 +10,5 @@ import com.sopt.clody.data.remote.dto.response.SignUpResponseDto interface AuthDataSource { suspend fun signIn(authorization: String, requestSignInDto: LoginRequestDto): ApiResponse suspend fun signUp(authorization: String, requestSignUpDto: SignUpRequestDto): ApiResponse + suspend fun signUpWithGoogle(googleSignUpRequestDto: GoogleSignUpRequestDto): ApiResponse } diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/AuthDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/AuthDataSourceImpl.kt index 91670e80..926d01a1 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/AuthDataSourceImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/AuthDataSourceImpl.kt @@ -3,6 +3,7 @@ package com.sopt.clody.data.remote.datasourceimpl import com.sopt.clody.data.remote.api.AuthService import com.sopt.clody.data.remote.datasource.AuthDataSource import com.sopt.clody.data.remote.dto.base.ApiResponse +import com.sopt.clody.data.remote.dto.request.GoogleSignUpRequestDto import com.sopt.clody.data.remote.dto.request.LoginRequestDto import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.dto.response.LoginResponseDto @@ -17,4 +18,7 @@ class AuthDataSourceImpl @Inject constructor( override suspend fun signUp(authorization: String, requestSignUpDto: SignUpRequestDto): ApiResponse = authService.signUp(authorization, requestSignUpDto) + + override suspend fun signUpWithGoogle(googleSignUpRequestDto: GoogleSignUpRequestDto): ApiResponse = + authService.signUpWithGoogle(googleSignUpRequestDto) } diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/request/GoogleSignUpRequestDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/request/GoogleSignUpRequestDto.kt new file mode 100644 index 00000000..a515e4d1 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/request/GoogleSignUpRequestDto.kt @@ -0,0 +1,12 @@ +package com.sopt.clody.data.remote.dto.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GoogleSignUpRequestDto( + @SerialName("idToken") val idToken: String, + @SerialName("platform") val platform: String, + @SerialName("fcmToken") val fcmToken: String, + @SerialName("name") val name: String?, +) diff --git a/app/src/main/java/com/sopt/clody/data/remote/util/AuthInterceptor.kt b/app/src/main/java/com/sopt/clody/data/remote/util/AuthInterceptor.kt index f3e1fac4..7ebc6934 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/util/AuthInterceptor.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/util/AuthInterceptor.kt @@ -49,6 +49,7 @@ class AuthInterceptor @Inject constructor( private fun shouldAddAuthorization(url: String): Boolean { return !url.contains("api/v1/auth/signin") && !url.contains("api/v1/auth/signup") && + !url.contains("api/v1/auth/oauth2/google") && !url.contains("api/v1/auth/reissue") } From 0fdb143074c9c928c25783c9b859538cc4b62b3c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 23 Jul 2025 23:04:38 +0900 Subject: [PATCH 259/299] =?UTF-8?q?[FEAT/#292]=20Google=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B8=B0=EB=8A=A5=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 AuthRepository와 AuthRepositoryImpl에 `signUpWithGoogle` 함수를 추가 --- .../sopt/clody/data/repositoryimpl/AuthRepositoryImpl.kt | 6 ++++++ .../java/com/sopt/clody/domain/repository/AuthRepository.kt | 2 ++ 2 files changed, 8 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/AuthRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/AuthRepositoryImpl.kt index 854fcaf7..824acfcd 100644 --- a/app/src/main/java/com/sopt/clody/data/repositoryimpl/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/AuthRepositoryImpl.kt @@ -1,6 +1,7 @@ package com.sopt.clody.data.repositoryimpl import com.sopt.clody.data.remote.datasource.AuthDataSource +import com.sopt.clody.data.remote.dto.request.GoogleSignUpRequestDto import com.sopt.clody.data.remote.dto.request.LoginRequestDto import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.dto.response.LoginResponseDto @@ -21,4 +22,9 @@ class AuthRepositoryImpl @Inject constructor( runCatching { authDataSource.signUp(authorization, requestSignUpDto).handleApiResponse().getOrThrow() } + + override suspend fun signUpWithGoogle(googleSignUpRequestDto: GoogleSignUpRequestDto): Result = + runCatching { + authDataSource.signUpWithGoogle(googleSignUpRequestDto).handleApiResponse().getOrThrow() + } } diff --git a/app/src/main/java/com/sopt/clody/domain/repository/AuthRepository.kt b/app/src/main/java/com/sopt/clody/domain/repository/AuthRepository.kt index e9b822ec..55f1053c 100644 --- a/app/src/main/java/com/sopt/clody/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/sopt/clody/domain/repository/AuthRepository.kt @@ -1,5 +1,6 @@ package com.sopt.clody.domain.repository +import com.sopt.clody.data.remote.dto.request.GoogleSignUpRequestDto import com.sopt.clody.data.remote.dto.request.LoginRequestDto import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.dto.response.LoginResponseDto @@ -8,4 +9,5 @@ import com.sopt.clody.data.remote.dto.response.SignUpResponseDto interface AuthRepository { suspend fun signIn(authorization: String, requestSignInDto: LoginRequestDto): Result suspend fun signUp(authorization: String, requestSignUpDto: SignUpRequestDto): Result + suspend fun signUpWithGoogle(googleSignUpRequestDto: GoogleSignUpRequestDto): Result } From 3932c570b793aa3778b5a88fb4f2a87850014949 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 23 Jul 2025 23:06:17 +0900 Subject: [PATCH 260/299] =?UTF-8?q?[ADD/#292]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=83=80=EC=9E=85=20=EB=B6=84=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sopt/clody/data/datastore/OAuthDataStore.kt | 1 - .../java/com/sopt/clody/data/datastore/OAuthProvider.kt | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/sopt/clody/data/datastore/OAuthProvider.kt diff --git a/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt b/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt index 443cc227..9fbaf319 100644 --- a/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt +++ b/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt @@ -3,7 +3,6 @@ package com.sopt.clody.data.datastore import android.content.Context import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStore -import com.sopt.clody.presentation.ui.login.OAuthProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.first import javax.inject.Inject diff --git a/app/src/main/java/com/sopt/clody/data/datastore/OAuthProvider.kt b/app/src/main/java/com/sopt/clody/data/datastore/OAuthProvider.kt new file mode 100644 index 00000000..c8c57020 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/datastore/OAuthProvider.kt @@ -0,0 +1,6 @@ +package com.sopt.clody.data.datastore + +enum class OAuthProvider(val apiValue: String) { + GOOGLE("google"), + KAKAO("kakao"), +} From 89ce95cacebd2ccd40e15450ae8857baae68c293 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 23 Jul 2025 23:07:41 +0900 Subject: [PATCH 261/299] =?UTF-8?q?[FEAT/#292]=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구글 로그인 기능을 구현했습니다. - GoogleSignInHelper 구현 - 404면 회원가입 프로세스 진행 - 추후 회원가입 때 어떤 타입의 회원가입(구글/카카오)인지 판단하기 위해 프로파이더로 분기 --- .../ui/login/GoogleSignInHelper.kt | 50 ++++++++++ .../presentation/ui/login/LoginContract.kt | 6 +- .../presentation/ui/login/LoginScreen.kt | 74 +++++++++------ .../presentation/ui/login/LoginViewModel.kt | 93 +++++++++++++------ 4 files changed, 160 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt new file mode 100644 index 00000000..79b39019 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt @@ -0,0 +1,50 @@ +package com.sopt.clody.presentation.ui.login + +import android.content.Context +import android.util.Log +import androidx.activity.result.IntentSenderRequest +import com.google.android.gms.auth.api.identity.BeginSignInRequest +import com.google.android.gms.auth.api.identity.Identity +import com.google.android.gms.auth.api.identity.SignInClient +import com.sopt.clody.BuildConfig + +class GoogleSignInHelper(context: Context) { + + private val signInClient: SignInClient = Identity.getSignInClient(context.applicationContext) + + fun buildSignInRequest(): BeginSignInRequest { + return BeginSignInRequest.Builder() + .setGoogleIdTokenRequestOptions( + BeginSignInRequest.GoogleIdTokenRequestOptions.builder() + .setSupported(true) + .setServerClientId(BuildConfig.GOOGLE_AUTH_WEB_CLIENT_ID) + .setFilterByAuthorizedAccounts(false) + .build(), + ) + .setAutoSelectEnabled(false) + .build() + } + + fun requestSignIn( + onSuccess: (IntentSenderRequest) -> Unit, + onFailure: (Exception) -> Unit, + ) { + val request = buildSignInRequest() + signInClient.beginSignIn(request) + .addOnSuccessListener { result -> + val intentSenderRequest = + IntentSenderRequest.Builder(result.pendingIntent.intentSender).build() + onSuccess(intentSenderRequest) + } + .addOnFailureListener { e -> + onFailure(e) + } + } + + fun extractIdToken(data: android.content.Intent?): String? { + return runCatching { + val credential = signInClient.getSignInCredentialFromIntent(data) + credential.googleIdToken + }.getOrNull() + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt index 1a9da0a9..dc2f2b8c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt @@ -2,19 +2,17 @@ package com.sopt.clody.presentation.ui.login import android.content.Context import com.airbnb.mvrx.MavericksState +import com.sopt.clody.data.datastore.OAuthProvider class LoginContract { data class LoginState( - val loginType: LoginType = LoginType.GOOGLE, val isLoading: Boolean = false, val errorMessage: String? = null, ) : MavericksState sealed class LoginIntent { - data object SetLoginType : LoginIntent() - data class LoginWithKakao(val context: Context) : LoginIntent() - data class LoginWithGoogle(val context: Context) : LoginIntent() + data class LoginOAuth(val provider: OAuthProvider, val context: Context? = null, val idToken: String? = null) : LoginIntent() data object ClearError : LoginIntent() } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt index 249a873c..be1a475e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt @@ -1,5 +1,8 @@ package com.sopt.clody.presentation.ui.login +import android.app.Activity +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -13,6 +16,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -24,10 +28,11 @@ import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.mavericksViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.sopt.clody.R -import com.sopt.clody.presentation.ui.auth.component.button.GoogleButton +import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.auth.component.button.KaKaoButton import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.dialog.FailureDialog +import com.sopt.clody.presentation.ui.login.LoginContract.LoginIntent import com.sopt.clody.presentation.utils.base.BasePreview import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage @@ -44,6 +49,18 @@ fun LoginRoute( val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current + val googleSignInHelper = remember { GoogleSignInHelper(context) } + + val googleSignInLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.StartIntentSenderForResult(), + ) { result -> + + if (result.resultCode == Activity.RESULT_OK) { + val idToken = googleSignInHelper.extractIdToken(result.data) + viewModel.postIntent(LoginIntent.LoginOAuth(OAuthProvider.GOOGLE, idToken = idToken)) + } + } + LaunchedEffect(viewModel) { lifecycleOwner.repeatOnStarted { viewModel.sideEffects.collect { sideEffect -> @@ -59,9 +76,18 @@ fun LoginRoute( } LoginScreen( - state = state, - onKaKaoLoginClick = { viewModel.postIntent(LoginContract.LoginIntent.LoginWithKakao(context)) }, - onGoogleLoginClick = { viewModel.postIntent(LoginContract.LoginIntent.LoginWithGoogle(context)) }, + isLoading = state.isLoading, + onKaKaoLoginClick = { + viewModel.postIntent(LoginIntent.LoginOAuth(OAuthProvider.KAKAO, context = context)) + }, + onLoginGoogleClick = { + googleSignInHelper.requestSignIn( + onSuccess = { intentSenderRequest -> googleSignInLauncher.launch(intentSenderRequest) }, + onFailure = { + viewModel.postIntent(LoginIntent.ClearError) + }, + ) + }, ) // 에러 메시지 추가로 다이얼로그로 처리하고 싶다면? @@ -74,9 +100,9 @@ fun LoginRoute( @Composable fun LoginScreen( - state: LoginContract.LoginState, + isLoading: Boolean, onKaKaoLoginClick: () -> Unit, - onGoogleLoginClick: () -> Unit, + onLoginGoogleClick: () -> Unit, ) { val systemUiController = rememberSystemUiController() val backgroundColor = ClodyTheme.colors.white @@ -90,27 +116,15 @@ fun LoginScreen( Scaffold( bottomBar = { - if (state.loginType == LoginType.KAKAO) { - KaKaoButton( - text = stringResource(id = R.string.signup_btn_kakao), - onClick = onKaKaoLoginClick, - modifier = Modifier - .navigationBarsPadding() - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(bottom = 40.dp), - ) - } else { - GoogleButton( - text = stringResource(id = R.string.signup_btn_google), - onClick = onGoogleLoginClick, - modifier = Modifier - .navigationBarsPadding() - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(bottom = 40.dp), - ) - } + KaKaoButton( + text = stringResource(id = R.string.signup_btn_kakao), + onClick = onLoginGoogleClick, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 40.dp), + ) }, ) { innerPadding -> Column( @@ -129,7 +143,7 @@ fun LoginScreen( } } - if (state.isLoading) { + if (isLoading) { LoadingScreen() } } @@ -139,9 +153,9 @@ fun LoginScreen( fun LoginScreenPreview() { BasePreview { LoginScreen( - state = LoginContract.LoginState(), + isLoading = false, onKaKaoLoginClick = {}, - onGoogleLoginClick = {}, + onLoginGoogleClick = {}, ) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt index 32ff7083..467602b5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -1,16 +1,18 @@ package com.sopt.clody.presentation.ui.login -import android.content.Context import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.core.login.LoginSdk +import com.sopt.clody.data.datastore.OAuthDataStore +import com.sopt.clody.data.datastore.OAuthProvider +import com.sopt.clody.data.remote.dto.request.GoogleSignUpRequestDto import com.sopt.clody.data.remote.dto.request.LoginRequestDto import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository -import com.sopt.clody.presentation.utils.language.LanguageProvider +import com.sopt.clody.presentation.ui.login.LoginContract.LoginIntent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -27,10 +29,10 @@ class LoginViewModel @AssistedInject constructor( private val authRepository: AuthRepository, private val tokenRepository: TokenRepository, private val fcmTokenProvider: FcmTokenProvider, - private val languageProvider: LanguageProvider, + private val oauthDataStore: OAuthDataStore, ) : MavericksViewModel(initialState) { - private val _intents = Channel(BUFFERED) + private val _intents = Channel(BUFFERED) private val _sideEffects = Channel(BUFFERED) val sideEffects = _sideEffects.receiveAsFlow() @@ -39,66 +41,99 @@ class LoginViewModel @AssistedInject constructor( .receiveAsFlow() .onEach(::handleIntent) .launchIn(viewModelScope) - postIntent(LoginContract.LoginIntent.SetLoginType) } - fun postIntent(intent: LoginContract.LoginIntent) { + fun postIntent(intent: LoginIntent) { viewModelScope.launch { _intents.send(intent) } } - private suspend fun handleIntent(intent: LoginContract.LoginIntent) { + private suspend fun handleIntent(intent: LoginIntent) { when (intent) { - is LoginContract.LoginIntent.SetLoginType -> { setState { copy(loginType = languageProvider.getLoginType()) } } - is LoginContract.LoginIntent.LoginWithKakao -> loginWithKakao(intent.context) - is LoginContract.LoginIntent.LoginWithGoogle -> loginWithGoogle(intent.context) - is LoginContract.LoginIntent.ClearError -> setState { copy(errorMessage = null) } + is LoginIntent.ClearError -> setState { copy(errorMessage = null) } + is LoginIntent.LoginOAuth -> handleLoginOAuth(intent) } } - private suspend fun loginWithKakao(context: Context) { + private suspend fun handleLoginOAuth(intent: LoginIntent.LoginOAuth) { setState { copy(isLoading = true, errorMessage = null) } - loginSdk.login(context).fold( - onSuccess = { accessToken -> - validateUser("Bearer ${accessToken.value}") + when (intent.provider) { + OAuthProvider.KAKAO -> { + loginSdk.login(intent.context!!).fold( + onSuccess = { token -> + validateKakaoUser(token.value) + }, + onFailure = { error -> + setState { copy(isLoading = false) } + _sideEffects.send(LoginContract.LoginSideEffect.ShowError("로그인에 실패했습니다")) + }, + ) + } + + OAuthProvider.GOOGLE -> { + val idToken = intent.idToken + if (idToken.isNullOrBlank()) { + _sideEffects.send(LoginContract.LoginSideEffect.ShowError("로그인에 실패했습니다.")) + return + } + + validateGoogleUser(idToken) + } + } + } + + private suspend fun validateKakaoUser(kakaoToken: String) { + val fcmToken = fcmTokenProvider.getToken().orEmpty() + val request = LoginRequestDto(platform = OAuthProvider.KAKAO.apiValue, fcmToken = fcmToken) + + authRepository.signIn("Bearer $kakaoToken", request).fold( + onSuccess = { + tokenRepository.setTokens(it.accessToken, it.refreshToken) + setState { copy(isLoading = false) } + _sideEffects.send(LoginContract.LoginSideEffect.NavigateToHome) }, onFailure = { error -> setState { copy(isLoading = false) } - _sideEffects.send( - LoginContract.LoginSideEffect.ShowError( - error.message ?: "로그인에 실패했습니다.", - ), - ) + val msg = error.message.orEmpty() + if (msg.contains("404") || msg.contains("유저가 없습니다")) { + _sideEffects.send(LoginContract.LoginSideEffect.NavigateToSignUp) + } else { + _sideEffects.send(LoginContract.LoginSideEffect.ShowError(msg)) + } }, ) } - private suspend fun validateUser(token: String) { + private suspend fun validateGoogleUser(idToken: String) { val fcmToken = fcmTokenProvider.getToken().orEmpty() - val request = LoginRequestDto(platform = "kakao", fcmToken = fcmToken) + val request = GoogleSignUpRequestDto( + idToken = idToken, + platform = OAuthProvider.GOOGLE.apiValue, + fcmToken = fcmToken, + name = null, + ) - authRepository.signIn(token, request).fold( + authRepository.signUpWithGoogle(request).fold( onSuccess = { tokenRepository.setTokens(it.accessToken, it.refreshToken) setState { copy(isLoading = false) } _sideEffects.send(LoginContract.LoginSideEffect.NavigateToHome) }, onFailure = { error -> - val message = error.message.orEmpty() setState { copy(isLoading = false) } - if (message.contains("404") || message.contains("유저가 없습니다")) { + val msg = error.message.orEmpty() + if (msg.contains("500") || msg.contains("유저가 없습니다")) { + oauthDataStore.saveIdToken(idToken) + oauthDataStore.savePlatform(OAuthProvider.GOOGLE) _sideEffects.send(LoginContract.LoginSideEffect.NavigateToSignUp) } else { - _sideEffects.send(LoginContract.LoginSideEffect.ShowError("로그인 실패")) + _sideEffects.send(LoginContract.LoginSideEffect.ShowError(msg)) } }, ) } - private suspend fun loginWithGoogle(context: Context) { - } - @AssistedFactory interface Factory : AssistedViewModelFactory { override fun create(state: LoginContract.LoginState): LoginViewModel From fcca07256e5b2f644a7bfcb793c2ee6b53278535 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 23 Jul 2025 23:08:49 +0900 Subject: [PATCH 262/299] =?UTF-8?q?[FEAT/#292]=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20API=20=EC=97=B0=EB=8F=99=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B5=AC=EA=B8=80=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회원가입 API 연동 - 회원가입 API에 구글 로그인 로직 추가 - Kakao와 Google 플랫폼에 따른 회원가입 분기 처리 --- .../ui/auth/signup/SignUpContract.kt | 5 +- .../ui/auth/signup/SignUpScreen.kt | 9 +- .../ui/auth/signup/SignUpViewModel.kt | 83 +++++++++++++------ 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt index 746f35e7..e94baaf4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt @@ -2,6 +2,7 @@ package com.sopt.clody.presentation.ui.auth.signup import android.content.Context import com.airbnb.mvrx.MavericksState +import com.sopt.clody.data.datastore.OAuthProvider class SignUpContract { data class SignUpState( @@ -15,8 +16,9 @@ class SignUpContract { val errorMessage: String? = null, val serviceChecked: Boolean = false, val serviceUrl: String = "", - val privacyChecked: Boolean = false, val privacyUrl: String = "", + val privacyChecked: Boolean = false, + val platform: OAuthProvider = OAuthProvider.KAKAO, ) : MavericksState { val allChecked: Boolean get() = serviceChecked && privacyChecked @@ -33,7 +35,6 @@ class SignUpContract { data object ProceedTerms : SignUpIntent() data class CompleteSignUp(val context: Context) : SignUpIntent() data object ClearError : SignUpIntent() - data class ToggleAllChecked(val checked: Boolean) : SignUpIntent() data class ToggleServiceChecked(val checked: Boolean) : SignUpIntent() data class TogglePrivacyChecked(val checked: Boolean) : SignUpIntent() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt index 0e744aa3..160e48f2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt @@ -29,13 +29,10 @@ fun SignUpRoute( viewModel.sideEffects.collect { effect -> when (effect) { is SignUpContract.SignUpSideEffect.NavigateToTimeReminder -> navigateToHome() - is SignUpContract.SignUpSideEffect.ShowMessage -> { - // TODO: Snackbar나 Dialog로 에러 메시지 처리 - } - is SignUpContract.SignUpSideEffect.NavigateToWebView -> { navigateToWebView(effect.url) // ✅ WebView 이동 처리 } + is SignUpContract.SignUpSideEffect.ShowMessage -> {} } } } @@ -83,7 +80,9 @@ fun SignUpScreen( NickNamePage( nickname = state.nickname, onNicknameChange = { onIntent(SignUpContract.SignUpIntent.SetNickname(it)) }, - onCompleteClick = { onIntent(SignUpContract.SignUpIntent.CompleteSignUp(context)) }, + onCompleteClick = { + onIntent(SignUpContract.SignUpIntent.CompleteSignUp(context)) + }, onBackClick = { onIntent(SignUpContract.SignUpIntent.BackToTerms) }, isLoading = state.isLoading, isValidNickname = state.isValidNickname, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index 0909bcd7..93d88f4e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -8,11 +8,15 @@ import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.airbnb.mvrx.withState import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.core.login.LoginSdk +import com.sopt.clody.data.remote.dto.request.GoogleSignUpRequestDto import com.sopt.clody.data.remote.dto.request.SignUpRequestDto +import com.sopt.clody.data.remote.dto.response.SignUpResponseDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository import com.sopt.clody.presentation.ui.auth.signup.SignUpContract.Companion.DEFAULT_NICKNAME_MESSAGE +import com.sopt.clody.data.datastore.OAuthDataStore +import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls import com.sopt.clody.presentation.utils.language.LanguageProvider import dagger.assisted.Assisted @@ -31,6 +35,7 @@ class SignUpViewModel @AssistedInject constructor( private val authRepository: AuthRepository, private val tokenRepository: TokenRepository, private val fcmTokenProvider: FcmTokenProvider, + private val oAuthDataStore: OAuthDataStore, private val networkUtil: NetworkUtil, private val languageProvider: LanguageProvider, ) : MavericksViewModel(initialState) { @@ -139,39 +144,69 @@ class SignUpViewModel @AssistedInject constructor( } } - private fun signUp(context: Context) { - viewModelScope.launch { - val state = withState(this@SignUpViewModel) { it } - if (!networkUtil.isNetworkAvailable()) { - setState { copy(errorMessage = "네트워크 연결을 확인해주세요.") } - return@launch + private suspend fun signUp(context: Context) { + val state = withState(this@SignUpViewModel) { it } + + if (!networkUtil.isNetworkAvailable()) { + setState { copy(errorMessage = "네트워크 연결을 확인해주세요.") } + return + } + + setState { copy(isLoading = true) } + + val platform = oAuthDataStore.getPlatform() + val fcmToken = fcmTokenProvider.getToken().orEmpty() + + if (platform == OAuthProvider.GOOGLE) { + val idToken = oAuthDataStore.getIdToken() + if (idToken.isNullOrBlank()) { + setState { copy(errorMessage = "Google ID Token이 없습니다.", isLoading = false) } + return } - setState { copy(isLoading = true) } + + val request = GoogleSignUpRequestDto( + idToken = idToken, + platform = "Android", + name = state.nickname, + fcmToken = fcmToken, + ) + + val result = authRepository.signUpWithGoogle(request) + handleSignUpResult(result, isGoogle = true) + } else { loginSdk.login(context).fold( - onSuccess = { accessToken -> - launch { - val fcm = fcmTokenProvider.getToken().orEmpty() - val req = SignUpRequestDto("kakao", state.nickname, fcm) - authRepository.signUp("Bearer ${accessToken.value}", req).fold( - onSuccess = { - tokenRepository.setTokens(it.accessToken, it.refreshToken) - _sideEffects.send(SignUpContract.SignUpSideEffect.NavigateToTimeReminder) - }, - onFailure = { - setState { copy(errorMessage = it.message ?: "알 수 없는 오류") } - }, - ) - - setState { copy(isLoading = false) } - } + onSuccess = { token -> + val request = SignUpRequestDto( + platform = OAuthProvider.KAKAO.apiValue, + name = state.nickname, + fcmToken = fcmToken, + ) + val result = authRepository.signUp("Bearer ${token.value}", request) + handleSignUpResult(result, isGoogle = false) }, onFailure = { - setState { copy(errorMessage = it.message ?: "로그인 실패", isLoading = false) } + setState { copy(errorMessage = "로그인에 실패했어요~", isLoading = false) } }, ) } } + private suspend fun handleSignUpResult( + result: Result, + isGoogle: Boolean, + ) { + result.fold( + onSuccess = { + tokenRepository.setTokens(it.accessToken, it.refreshToken) + if (isGoogle) oAuthDataStore.clear() + _sideEffects.send(SignUpContract.SignUpSideEffect.NavigateToTimeReminder) + }, + onFailure = { + setState { copy(errorMessage = "회원가입에 실패했어요~") } + }, + ) + setState { copy(isLoading = false) } + } private fun validateNickname(nickname: String): Boolean { val state = withState(this@SignUpViewModel) { it } setState { copy(nicknameMaxLength = languageProvider.getNicknameMaxLength()) } From c7fdbb0a96622bc13f9018a57d0c53e48a1cd887 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Sat, 26 Jul 2025 17:22:11 +0900 Subject: [PATCH 263/299] =?UTF-8?q?[REFACTOR/#292]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20API=20=EC=9A=94=EC=B2=AD=20=EC=8B=9C=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EA=B0=92=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 로그인 API 요청 시 불필요한 platform, name 값 제거 GoogleSignUpRequestDto의 platform, name 제거 --- .../dto/request/GoogleSignUpRequestDto.kt | 2 - .../ui/auth/signup/SignUpViewModel.kt | 13 ++--- .../ui/login/GoogleSignInHelper.kt | 1 - .../presentation/ui/login/LoginContract.kt | 1 + .../presentation/ui/login/LoginScreen.kt | 52 ++++++++++++------- .../presentation/ui/login/LoginViewModel.kt | 2 - 6 files changed, 38 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/request/GoogleSignUpRequestDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/request/GoogleSignUpRequestDto.kt index a515e4d1..f4ed6c28 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/request/GoogleSignUpRequestDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/request/GoogleSignUpRequestDto.kt @@ -6,7 +6,5 @@ import kotlinx.serialization.Serializable @Serializable data class GoogleSignUpRequestDto( @SerialName("idToken") val idToken: String, - @SerialName("platform") val platform: String, @SerialName("fcmToken") val fcmToken: String, - @SerialName("name") val name: String?, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index 93d88f4e..f96389c0 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -8,15 +8,14 @@ import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.airbnb.mvrx.withState import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.core.login.LoginSdk -import com.sopt.clody.data.remote.dto.request.GoogleSignUpRequestDto +import com.sopt.clody.data.datastore.OAuthDataStore +import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.dto.response.SignUpResponseDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository import com.sopt.clody.presentation.ui.auth.signup.SignUpContract.Companion.DEFAULT_NICKNAME_MESSAGE -import com.sopt.clody.data.datastore.OAuthDataStore -import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls import com.sopt.clody.presentation.utils.language.LanguageProvider import dagger.assisted.Assisted @@ -163,15 +162,13 @@ class SignUpViewModel @AssistedInject constructor( setState { copy(errorMessage = "Google ID Token이 없습니다.", isLoading = false) } return } - - val request = GoogleSignUpRequestDto( - idToken = idToken, - platform = "Android", + val request = SignUpRequestDto( + platform = OAuthProvider.GOOGLE.apiValue, name = state.nickname, fcmToken = fcmToken, ) - val result = authRepository.signUpWithGoogle(request) + val result = authRepository.signUp("Bearer $idToken", request) handleSignUpResult(result, isGoogle = true) } else { loginSdk.login(context).fold( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt index 79b39019..67dc80f1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt @@ -1,7 +1,6 @@ package com.sopt.clody.presentation.ui.login import android.content.Context -import android.util.Log import androidx.activity.result.IntentSenderRequest import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.Identity diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt index dc2f2b8c..9daaa695 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginContract.kt @@ -7,6 +7,7 @@ import com.sopt.clody.data.datastore.OAuthProvider class LoginContract { data class LoginState( + val loginType: OAuthProvider? = null, val isLoading: Boolean = false, val errorMessage: String? = null, ) : MavericksState diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt index be1a475e..476e81be 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt @@ -29,8 +29,8 @@ import com.airbnb.mvrx.compose.mavericksViewModel import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.sopt.clody.R import com.sopt.clody.data.datastore.OAuthProvider +import com.sopt.clody.presentation.ui.auth.component.button.GoogleButton import com.sopt.clody.presentation.ui.auth.component.button.KaKaoButton -import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.dialog.FailureDialog import com.sopt.clody.presentation.ui.login.LoginContract.LoginIntent import com.sopt.clody.presentation.utils.base.BasePreview @@ -76,11 +76,11 @@ fun LoginRoute( } LoginScreen( - isLoading = state.isLoading, + state = state, onKaKaoLoginClick = { viewModel.postIntent(LoginIntent.LoginOAuth(OAuthProvider.KAKAO, context = context)) }, - onLoginGoogleClick = { + onGoogleLoginClick = { googleSignInHelper.requestSignIn( onSuccess = { intentSenderRequest -> googleSignInLauncher.launch(intentSenderRequest) }, onFailure = { @@ -100,9 +100,9 @@ fun LoginRoute( @Composable fun LoginScreen( - isLoading: Boolean, + state: LoginContract.LoginState, onKaKaoLoginClick: () -> Unit, - onLoginGoogleClick: () -> Unit, + onGoogleLoginClick: () -> Unit, ) { val systemUiController = rememberSystemUiController() val backgroundColor = ClodyTheme.colors.white @@ -116,15 +116,27 @@ fun LoginScreen( Scaffold( bottomBar = { - KaKaoButton( - text = stringResource(id = R.string.signup_btn_kakao), - onClick = onLoginGoogleClick, - modifier = Modifier - .navigationBarsPadding() - .fillMaxWidth() - .padding(horizontal = 24.dp) - .padding(bottom = 40.dp), - ) + if (state.loginType == OAuthProvider.KAKAO) { + KaKaoButton( + text = stringResource(id = R.string.signup_btn_kakao), + onClick = onKaKaoLoginClick, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 40.dp), + ) + } else { + GoogleButton( + text = stringResource(id = R.string.signup_btn_google), + onClick = onGoogleLoginClick, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 40.dp), + ) + } }, ) { innerPadding -> Column( @@ -142,10 +154,6 @@ fun LoginScreen( ) } } - - if (isLoading) { - LoadingScreen() - } } @ClodyPreview @@ -153,9 +161,13 @@ fun LoginScreen( fun LoginScreenPreview() { BasePreview { LoginScreen( - isLoading = false, + state = LoginContract.LoginState( + isLoading = false, + loginType = OAuthProvider.KAKAO, + errorMessage = null, + ), onKaKaoLoginClick = {}, - onLoginGoogleClick = {}, + onGoogleLoginClick = {}, ) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt index 467602b5..94029f6f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -108,9 +108,7 @@ class LoginViewModel @AssistedInject constructor( val fcmToken = fcmTokenProvider.getToken().orEmpty() val request = GoogleSignUpRequestDto( idToken = idToken, - platform = OAuthProvider.GOOGLE.apiValue, fcmToken = fcmToken, - name = null, ) authRepository.signUpWithGoogle(request).fold( From ff0653b673a9b7c50270d1ead7b9b4fd0df80fe2 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 28 Jul 2025 22:39:51 +0900 Subject: [PATCH 264/299] =?UTF-8?q?[REFACTOR/#292]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=83=80=EC=9E=85=20=EB=A1=9C=EC=A7=81=20LanguageP?= =?UTF-8?q?rovider=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LanguageProvider를 통해 로그인 타입 결정 - LanguageProviderImpl에서 시스템 언어 설정에 따라 로그인 타입 반환 --- .../sopt/clody/presentation/ui/login/LoginViewModel.kt | 3 +++ .../presentation/utils/language/LanguageProvider.kt | 4 ++-- .../presentation/utils/language/LanguageProviderImpl.kt | 9 ++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt index 94029f6f..e5d53217 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -13,6 +13,7 @@ import com.sopt.clody.data.remote.dto.request.LoginRequestDto import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository import com.sopt.clody.presentation.ui.login.LoginContract.LoginIntent +import com.sopt.clody.presentation.utils.language.LanguageProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -30,6 +31,7 @@ class LoginViewModel @AssistedInject constructor( private val tokenRepository: TokenRepository, private val fcmTokenProvider: FcmTokenProvider, private val oauthDataStore: OAuthDataStore, + private val languageProvider: LanguageProvider, ) : MavericksViewModel(initialState) { private val _intents = Channel(BUFFERED) @@ -37,6 +39,7 @@ class LoginViewModel @AssistedInject constructor( val sideEffects = _sideEffects.receiveAsFlow() init { + setState { copy(loginType = languageProvider.getLoginType()) } _intents .receiveAsFlow() .onEach(::handleIntent) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt index 1ca49f26..4c697c74 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt @@ -1,10 +1,10 @@ package com.sopt.clody.presentation.utils.language -import com.sopt.clody.presentation.ui.login.LoginType +import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls interface LanguageProvider { - fun getLoginType(): LoginType + fun getLoginType(): OAuthProvider fun getNicknameMaxLength(): Int fun getDiaryMaxLength(): Int fun getWebViewUrlFor(option: SettingOptionUrls): String diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt index 0a1976d2..6bf0749f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt @@ -1,7 +1,7 @@ package com.sopt.clody.presentation.utils.language import android.content.Context -import com.sopt.clody.presentation.ui.login.LoginType +import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Locale @@ -15,8 +15,11 @@ class LanguageProviderImpl @Inject constructor( private fun isKorean(): Boolean = locale.language == LANGUAGE_KO - override fun getLoginType(): LoginType { - return if (isKorean()) LoginType.KAKAO else LoginType.GOOGLE + override fun getLoginType(): OAuthProvider { + return when (locale.language) { + LANGUAGE_KO -> OAuthProvider.KAKAO + else -> OAuthProvider.GOOGLE + } } override fun getNicknameMaxLength(): Int { From ba767dff3406669d3a47230a9d612ea18e2f1663 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 28 Jul 2025 23:00:15 +0900 Subject: [PATCH 265/299] =?UTF-8?q?[FEAT/#292]=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=94=8C=EB=9E=AB=ED=8F=BC=20=EB=B3=84=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserInfoResponseDto의 platform 타입을 OAuthProvider로 변경 - AccountManagementViewModel에서 platform Enum 처리 - AccountManagementLogoutOption에서 로그인 플랫폼에 따라 아이콘을 다르게 표시 --- .../remote/dto/response/UserInfoResponseDto.kt | 3 ++- .../component/AccountManagementLogoutOption.kt | 16 +++++++++++----- .../ui/setting/screen/AccountManagementScreen.kt | 11 +++++------ .../setting/screen/AccountManagementViewModel.kt | 3 ++- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/UserInfoResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/UserInfoResponseDto.kt index 558e2797..ba4297e9 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/UserInfoResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/UserInfoResponseDto.kt @@ -1,5 +1,6 @@ package com.sopt.clody.data.remote.dto.response +import com.sopt.clody.data.datastore.OAuthProvider import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -7,5 +8,5 @@ import kotlinx.serialization.Serializable data class UserInfoResponseDto( @SerialName("email") val email: String, @SerialName("name") val name: String, - @SerialName("platform") val platform: String, + @SerialName("platform") val platform: OAuthProvider?, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt index 927b01be..435d7fcf 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt @@ -16,21 +16,27 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sopt.clody.R +import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.ui.theme.ClodyTheme @Composable fun AccountManagementLogoutOption( userEmail: String, + platform: OAuthProvider?, updateLogoutDialog: (Boolean) -> Unit, ) { + val platformIconRes = when (platform) { + OAuthProvider.KAKAO -> R.drawable.img_account_management_kakao + OAuthProvider.GOOGLE -> R.drawable.img_google_button_logo + else -> R.drawable.img_google_button_logo // 서버에서 google을 어떻게 내려줄까요? + } + Row( - modifier = Modifier - .padding(top = 12.dp, bottom = 24.dp, start = 22.dp, end = 24.dp), + modifier = Modifier.padding(top = 12.dp, bottom = 24.dp, start = 22.dp, end = 24.dp), ) { Image( - painter = painterResource(id = R.drawable.img_account_management_kakao), - modifier = Modifier - .size(24.dp), + painter = painterResource(id = platformIconRes), + modifier = Modifier.size(24.dp), contentDescription = null, ) Spacer(modifier = Modifier.width(10.dp)) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt index 168dbfbb..70e258d4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt @@ -135,12 +135,11 @@ fun AccountManagementScreen( updateNicknameChangeBottomSheet = updateNicknameChangeBottomSheet, ) - if (userInfo.platform == "kakao") { - AccountManagementLogoutOption( - userEmail = userInfo.email, - updateLogoutDialog = updateLogoutDialog, - ) - } + AccountManagementLogoutOption( + userEmail = userInfo.email, + platform = userInfo.platform, + updateLogoutDialog = updateLogoutDialog, + ) SettingSeparateLine() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt index 26efca67..95f0afde 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt @@ -67,8 +67,9 @@ class AccountManagementViewModel @Inject constructor( val result = accountManagementRepository.getUserInfo() _userInfoState.value = result.fold( onSuccess = { + val platformEnum = it.platform retryCount = 0 - UserInfoState.Success(it) + UserInfoState.Success(it.copy(platform = platformEnum)) }, onFailure = { retryCount++ From fa26d569fea156b660ef439decebc0d16ed09818 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 29 Jul 2025 16:26:29 +0900 Subject: [PATCH 266/299] =?UTF-8?q?[REFACTOR/#309]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20API=EB=A5=BC=20TimeZone=EC=9D=84=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=ED=95=98=EC=97=AC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20KST(=EC=84=9C=EB=B2=84)=20<-?= =?UTF-8?q?>=20UST(User's=20Time=20Zone)=EC=9D=84=20=EB=B3=80=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=95=A8=EC=88=98=EB=A5=BC=20=EC=A0=9C?= =?UTF-8?q?=EC=9E=91=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/timereminder/TimeReminderScreen.kt | 2 +- .../timereminder/TimeReminderViewModel.kt | 16 +----- .../ui/home/screen/HomeViewModel.kt | 10 +++- .../NotificationSettingTimePicker.kt | 12 ++-- .../component/NotificationTimeSelector.kt | 5 +- .../screen/NotificationSettingScreen.kt | 11 ++-- .../screen/NotificationSettingViewModel.kt | 6 +- .../utils/extension/TimeZoneExt.kt | 56 +++++++++++++++++++ app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 10 files changed, 87 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt index ad699cdc..a00ce254 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt @@ -102,7 +102,7 @@ fun TimeReminderRoute( TimeReminderScreen( onStartClick = { - viewModel.setFixedTime(TimePeriod.PM, "9", "30") + viewModel.setSelectedTime(TimePeriod.PM, "9", "30") viewModel.sendNotification(context, isNotificationPermissionGranted.value) }, onTimeSelected = { period, hour, minute -> diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt index 0d07fbec..6e34cc55 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt @@ -10,6 +10,7 @@ import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.presentation.utils.extension.TimePeriod +import com.sopt.clody.presentation.utils.extension.convertUTZtoKST import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR @@ -17,7 +18,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import java.util.Locale import javax.inject.Inject @HiltViewModel @@ -80,18 +80,6 @@ class TimeReminderViewModel @Inject constructor( } fun setSelectedTime(period: TimePeriod, hour: String, minute: String) { - selectedTime = formatTime(period, hour, minute) - } - - fun setFixedTime(period: TimePeriod, hour: String, minute: String) { - selectedTime = formatTime(period, hour, minute) - } - - private fun formatTime(period: TimePeriod, hour: String, minute: String): String { - val hourInt = when (period) { - TimePeriod.PM -> if (hour.toInt() != 12) hour.toInt() + 12 else 12 - TimePeriod.AM -> if (hour.toInt() == 12) 0 else hour.toInt() - } - return String.format(Locale.US, "%02d:%02d", hourInt, minute.toInt()) + selectedTime = convertUTZtoKST(timePeriod = period, hour = hour, minute = minute) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index fd3d3b81..3518c1ba 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import java.time.LocalDate +import java.time.ZoneId import javax.inject.Inject @HiltViewModel @@ -232,9 +233,14 @@ class HomeViewModel @Inject constructor( } fun canWriteDiary(): Boolean { + val userTimeZone = ZoneId.systemDefault().id val today = LocalDate.now() val selected = _selectedDate.value - val isAvailableDay = selected == today || selected == today.minusDays(1) + val isAvailableDay = if (userTimeZone == "Asia/Seoul") { + selected == today || selected == today.minusDays(1) + } else { + selected == today + } return _diaryCount.value == 0 && isAvailableDay } @@ -276,7 +282,7 @@ class HomeViewModel @Inject constructor( isDiaryAlarm = info.isDiaryAlarm, isDraftAlarm = true, isReplyAlarm = info.isReplyAlarm, - time = info.time.ifEmpty { "21:30" }, + time = info.time, fcmToken = fcmToken, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt index ccf2a97c..145397a1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt @@ -28,13 +28,13 @@ import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.timepicker.ClodyPicker import com.sopt.clody.presentation.ui.component.timepicker.rememberPickerState -import com.sopt.clody.presentation.utils.extension.to24HourFormat +import com.sopt.clody.presentation.utils.extension.TimePeriod import com.sopt.clody.ui.theme.ClodyTheme @Composable fun NotificationSettingTimePicker( onDismissRequest: () -> Unit, - onConfirm: (String) -> Unit, + onConfirm: (TimePeriod, String, String) -> Unit, ) { val amPmItems = remember { listOf("오전", "오후") } val hourItems = remember { (1..12).map { it.toString() } } @@ -137,12 +137,8 @@ fun NotificationSettingTimePicker( } ClodyButton( onClick = { - val selectedTime = Triple( - amPmPickerState.selectedItem, - hourPickerState.selectedItem, - minutePickerState.selectedItem, - ).to24HourFormat() - onConfirm(selectedTime) + val selectedPeriod = if (amPmPickerState.selectedItem == "오전") TimePeriod.AM else TimePeriod.PM + onConfirm(selectedPeriod, hourPickerState.selectedItem, minutePickerState.selectedItem) }, text = stringResource(R.string.bottom_sheet_notification_time_change_confirm), enabled = true, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationTimeSelector.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationTimeSelector.kt index acaaf484..4bf314b4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationTimeSelector.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationTimeSelector.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.sopt.clody.R +import com.sopt.clody.presentation.utils.extension.convertKSTtoUTZ import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -22,6 +23,8 @@ fun NotificationTimeSelector( onClick: () -> Unit, modifier: Modifier = Modifier, ) { + val timeUST = convertKSTtoUTZ(time) + Row( modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -41,7 +44,7 @@ fun NotificationTimeSelector( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = time, + text = stringResource(R.string.notification_setting_selected_time, timeUST.first.getLabel(), timeUST.second, timeUST.third), style = ClodyTheme.typography.body3Medium, color = ClodyTheme.colors.gray05, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index f0410dde..064424fd 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -32,7 +32,6 @@ import com.sopt.clody.presentation.ui.setting.notificationsetting.component.Noti import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationSwitch import com.sopt.clody.presentation.ui.setting.notificationsetting.component.NotificationTimeSelector import com.sopt.clody.presentation.utils.base.ClodyPreview -import com.sopt.clody.presentation.utils.extension.convertTo12HourFormat import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -66,11 +65,13 @@ fun NotificationSettingRoute( ) if (showNotificationTimePicker) { - ClodyPopupBottomSheet(onDismissRequest = { showNotificationTimePicker = false }) { + ClodyPopupBottomSheet( + onDismissRequest = { showNotificationTimePicker = false }, + ) { NotificationSettingTimePicker( onDismissRequest = { showNotificationTimePicker = false }, - onConfirm = { newNotificationTime -> - notificationSettingViewModel.changeNotificationTime(context, newNotificationTime) + onConfirm = { timePeriod, hour, minute -> + notificationSettingViewModel.changeNotificationTime(context, timePeriod, hour, minute) showNotificationTimePicker = false }, ) @@ -153,7 +154,7 @@ fun NotificationSettingScreen( ) Spacer(modifier = Modifier.height(24.dp)) NotificationTimeSelector( - time = notificationTime.convertTo12HourFormat(), + time = notificationTime, onClick = onClickNotificationTime, ) Spacer(modifier = Modifier.height(24.dp)) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt index 20a90efc..c29f3268 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt @@ -7,6 +7,8 @@ import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.Notification import com.sopt.clody.domain.repository.NotificationRepository +import com.sopt.clody.presentation.utils.extension.TimePeriod +import com.sopt.clody.presentation.utils.extension.convertUTZtoKST import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR @@ -140,7 +142,7 @@ class NotificationSettingViewModel @Inject constructor( } } - fun changeNotificationTime(context: Context, time: String) { + fun changeNotificationTime(context: Context, timePeriod: TimePeriod, hour: String, minute: String) { _notificationTimeChangeState.value = NotificationTimeChangeState.Loading viewModelScope.launch { if (!networkUtil.isNetworkAvailable()) { @@ -157,7 +159,7 @@ class NotificationSettingViewModel @Inject constructor( isDiaryAlarm = _diaryAlarm.value, isDraftAlarm = _draftAlarm.value, isReplyAlarm = _replyAlarm.value, - time = time, + time = convertUTZtoKST(timePeriod, hour, minute), fcmToken = fcmToken, ) notificationRepository.sendNotification(requestDto).fold( diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt new file mode 100644 index 00000000..4ec0c779 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt @@ -0,0 +1,56 @@ +package com.sopt.clody.presentation.utils.extension + +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +/** +* @param time 서버로부터 수신받은 시간으로 "21:30" 와 같은 형태로 전달받는다. +* */ +fun convertKSTtoUTZ(time: String): Triple { + val kstZoneId = ZoneId.of("Asia/Seoul") + val userZoneId = ZoneId.systemDefault() + + val formatter = DateTimeFormatter.ofPattern("HH:mm") + val localTime = LocalTime.parse(time, formatter) + + val kstDateTime = ZonedDateTime.of(LocalDate.now(kstZoneId), localTime, kstZoneId) + val userDateTime = kstDateTime.withZoneSameInstant(userZoneId) + + val hour24 = userDateTime.hour + val minute = userDateTime.minute + val timePeriod = if (hour24 < 12) TimePeriod.AM else TimePeriod.PM + val hour12 = when { + hour24 == 0 -> 12 + hour24 > 12 -> hour24 - 12 + else -> hour24 + } + + return Triple(timePeriod, hour12, minute) +} + +/** + * @param timePeriod 오전/오후 + * @param hour 시간 + * @param minute 분 + * */ +fun convertUTZtoKST(timePeriod: TimePeriod, hour: String, minute: String): String { + val userZoneId = ZoneId.systemDefault() + val kstZoneId = ZoneId.of("Asia/Seoul") + + val hour24 = when (timePeriod) { + TimePeriod.AM -> if (hour == "12") 0 else hour.toInt() + TimePeriod.PM -> if (hour == "12") 12 else (hour + 12).toInt() + } + + val userTime = LocalTime.of(hour24, minute.toInt()) + val userZoned = ZonedDateTime.of(LocalDate.now(userZoneId), userTime, userZoneId) + val kstZoned = userZoned.withZoneSameInstant(kstZoneId) + + val kstHour = kstZoned.hour + val kstMinute = kstZoned.minute + + return String.format("%02s:%02s", kstHour, kstMinute) +} diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index daad26b3..d673c5b7 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -104,7 +104,7 @@ 일기 작성 알림 받기 이어쓰기 알림 받기 알림 시간 - %1$s %2$d시 %3$s분 + %1$s %2$d시 %3$d분 답장 도착 알림 받기 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee023ea5..f2dbef4e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,7 +104,7 @@ Journal Reminder Draft Reminder Reminder Time - %2$d:%3$s %1$s + %2$d:%3$d %1$s Reply Notification From 348cfc0f8c5593206bed6f8b7bf400a5ca22e631 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 29 Jul 2025 16:26:59 +0900 Subject: [PATCH 267/299] =?UTF-8?q?[REFACTOR/#309]=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?CRUD,=20=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5,=20=EB=8B=B5?= =?UTF-8?q?=EC=9E=A5=20=EA=B4=80=EB=A0=A8=20API=EB=93=A4=EC=97=90=20TimeZo?= =?UTF-8?q?ne=20=ED=97=A4=EB=8D=94=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20Interceptor=EB=A1=9C=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/remote/util/TimeZoneInterceptor.kt | 39 +++++++++++++++++++ .../java/com/sopt/clody/di/NetworkModule.kt | 7 ++++ 2 files changed, 46 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/data/remote/util/TimeZoneInterceptor.kt diff --git a/app/src/main/java/com/sopt/clody/data/remote/util/TimeZoneInterceptor.kt b/app/src/main/java/com/sopt/clody/data/remote/util/TimeZoneInterceptor.kt new file mode 100644 index 00000000..c1ee58a7 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/remote/util/TimeZoneInterceptor.kt @@ -0,0 +1,39 @@ +package com.sopt.clody.data.remote.util + +import okhttp3.Interceptor +import okhttp3.Response +import java.time.ZoneId +import javax.inject.Inject + +class TimeZoneInterceptor @Inject constructor() : Interceptor { + + private val userTimeZone = ZoneId.systemDefault().id + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + val url = originalRequest.url.toString() + + return if (shouldAddTimeZoneHeader(url)) { + proceedWithTimeZoneHeader(chain, originalRequest) + } else { + chain.proceed(originalRequest) + } + } + + private fun shouldAddTimeZoneHeader(url: String) = + url.contains("api/v1/diary") || + url.contains("api/v1/diary/time") || + url.contains("api/v1/calendar") || + url.contains("api/v1/calendar/list") || + url.contains("api/v1/reply") || + url.contains("api/v1/reply/ad/start") || + url.contains("api/v1/reply/ad/end") || + url.contains("api/v1/draft") + + private fun proceedWithTimeZoneHeader(chain: Interceptor.Chain, request: okhttp3.Request): Response = + chain.proceed(request.newBuilder().addHeader(TIME_ZONE, userTimeZone).build()) + + companion object { + private const val TIME_ZONE = "Time-Zone" + } +} diff --git a/app/src/main/java/com/sopt/clody/di/NetworkModule.kt b/app/src/main/java/com/sopt/clody/di/NetworkModule.kt index 4399784d..da77ddda 100644 --- a/app/src/main/java/com/sopt/clody/di/NetworkModule.kt +++ b/app/src/main/java/com/sopt/clody/di/NetworkModule.kt @@ -7,6 +7,7 @@ import com.sopt.clody.BuildConfig import com.sopt.clody.data.datastore.TokenDataStore import com.sopt.clody.data.remote.util.AuthInterceptor import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.data.remote.util.TimeZoneInterceptor import com.sopt.clody.domain.repository.TokenReissueRepository import dagger.Module import dagger.Provides @@ -41,15 +42,21 @@ object NetworkModule { return AuthInterceptor(tokenReissueRepository, tokenDataStore, context) } + @Provides + @Singleton + fun provideTimeZoneInterceptor(): TimeZoneInterceptor = TimeZoneInterceptor() + @Provides @Singleton fun provideClodyOkHttpClient( loggingInterceptor: HttpLoggingInterceptor, oauthInterceptor: AuthInterceptor, + timeZoneInterceptor: TimeZoneInterceptor, ): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .addInterceptor(oauthInterceptor) + .addInterceptor(timeZoneInterceptor) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() From c0df468d88836bf45167c1d9a86a8438bd4b5387 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 29 Jul 2025 19:20:33 +0900 Subject: [PATCH 268/299] =?UTF-8?q?[CHORE/#309]=20=EC=A0=84=EB=82=A0=20?= =?UTF-8?q?=EC=9D=BC=EA=B8=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=98=81?= =?UTF-8?q?=EC=96=B4=20=EB=B2=84=EC=A0=84=20UX=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=ED=8C=85=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f2dbef4e..97534fad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,7 +31,7 @@ Each reply comes\nwith a lucky four-leaf clover The more you confide in Lody\nthe luckier your clover becomes - You can only journal\nfor today and yesterday + You can only\njournal for today Past or future dates aren\'t available,\nDon\'t miss your moment Ready to write\nyour first journal?\nI\'ll be waiting! From 8f0e438e858fbff6a762ad9a593725e872f87cd2 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 29 Jul 2025 21:52:31 +0900 Subject: [PATCH 269/299] =?UTF-8?q?[CHORE/#309]=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A0=88=EB=B9=97=20=EB=A6=AC=EB=B7=B0=EC=82=AC=ED=95=AD?= =?UTF-8?q?=EC=9D=84=20=EB=B0=98=EC=98=81=ED=95=A9=EB=8B=88=EB=8B=A4.=20-?= =?UTF-8?q?=20referenceDate=20=EC=A3=BC=EC=9E=85=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=95=88=EC=A0=95=EC=84=B1=20=EC=A6=9D=EA=B0=80=20(=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=20=EA=B8=B0=EC=A4=80=EC=9D=BC=EC=9E=90=20=EB=B3=B4?= =?UTF-8?q?=EC=9E=A5,=20=EB=82=A0=EC=A7=9C=20=EA=B2=BD=EA=B3=84=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EB=B0=A9,=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9A=A9=EC=9D=B4?= =?UTF-8?q?)=20-=20hour,=20minute=20=EC=9D=84=20Int=20->=20String=20?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=83=80=EC=9E=85=20=EB=B0=8F=20=ED=8F=AC?= =?UTF-8?q?=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 --- .../utils/extension/TimeZoneExt.kt | 18 +++++++++++------- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt index 4ec0c779..b3b6832d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt @@ -9,18 +9,19 @@ import java.time.format.DateTimeFormatter /** * @param time 서버로부터 수신받은 시간으로 "21:30" 와 같은 형태로 전달받는다. * */ -fun convertKSTtoUTZ(time: String): Triple { +fun convertKSTtoUTZ(time: String, referenceDate: LocalDate = LocalDate.now()): Triple { val kstZoneId = ZoneId.of("Asia/Seoul") val userZoneId = ZoneId.systemDefault() val formatter = DateTimeFormatter.ofPattern("HH:mm") val localTime = LocalTime.parse(time, formatter) - val kstDateTime = ZonedDateTime.of(LocalDate.now(kstZoneId), localTime, kstZoneId) + val kstDateTime = ZonedDateTime.of(referenceDate, localTime, kstZoneId) val userDateTime = kstDateTime.withZoneSameInstant(userZoneId) val hour24 = userDateTime.hour val minute = userDateTime.minute + val timePeriod = if (hour24 < 12) TimePeriod.AM else TimePeriod.PM val hour12 = when { hour24 == 0 -> 12 @@ -28,7 +29,10 @@ fun convertKSTtoUTZ(time: String): Triple { else -> hour24 } - return Triple(timePeriod, hour12, minute) + val hourFormatted = hour12.toString() + val minuteFormatted = String.format("%02d", minute) + + return Triple(timePeriod, hourFormatted, minuteFormatted) } /** @@ -36,21 +40,21 @@ fun convertKSTtoUTZ(time: String): Triple { * @param hour 시간 * @param minute 분 * */ -fun convertUTZtoKST(timePeriod: TimePeriod, hour: String, minute: String): String { +fun convertUTZtoKST(timePeriod: TimePeriod, hour: String, minute: String, referenceDate: LocalDate = LocalDate.now()): String { val userZoneId = ZoneId.systemDefault() val kstZoneId = ZoneId.of("Asia/Seoul") val hour24 = when (timePeriod) { TimePeriod.AM -> if (hour == "12") 0 else hour.toInt() - TimePeriod.PM -> if (hour == "12") 12 else (hour + 12).toInt() + TimePeriod.PM -> if (hour == "12") 12 else hour.toInt() + 12 } val userTime = LocalTime.of(hour24, minute.toInt()) - val userZoned = ZonedDateTime.of(LocalDate.now(userZoneId), userTime, userZoneId) + val userZoned = ZonedDateTime.of(referenceDate, userTime, userZoneId) val kstZoned = userZoned.withZoneSameInstant(kstZoneId) val kstHour = kstZoned.hour val kstMinute = kstZoned.minute - return String.format("%02s:%02s", kstHour, kstMinute) + return String.format(java.util.Locale.ROOT, "%02d:%02d", kstHour, kstMinute) } diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index d673c5b7..c03143be 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -104,7 +104,7 @@ 일기 작성 알림 받기 이어쓰기 알림 받기 알림 시간 - %1$s %2$d시 %3$d분 + %1$s %2$s시 %3$s분 답장 도착 알림 받기 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 97534fad..9b0a8d8d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,7 +104,7 @@ Journal Reminder Draft Reminder Reminder Time - %2$d:%3$d %1$s + %2$s:%3$s %1$s Reply Notification From 8f766efdccc8969076b5ea82f56fed320220799a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Wed, 30 Jul 2025 22:38:21 +0900 Subject: [PATCH 270/299] =?UTF-8?q?[REFACTOR/#292]=20SplashState=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20isUserLogged?= =?UTF-8?q?In=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sopt/clody/presentation/ui/splash/SplashContract.kt | 1 - .../com/sopt/clody/presentation/ui/splash/SplashViewModel.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt index c120e0c0..ae19ecff 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashContract.kt @@ -6,7 +6,6 @@ import com.sopt.clody.domain.model.AppUpdateState class SplashContract { data class SplashState( - val isUserLoggedIn: Boolean? = null, val updateState: AppUpdateState? = null, val showInspectionDialog: Boolean = false, val inspectionTimeText: String? = null, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt index f17f5988..db366f59 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/splash/SplashViewModel.kt @@ -79,7 +79,6 @@ class SplashViewModel @AssistedInject constructor( private fun attemptAutoLogin(): Boolean { val isLoggedIn = tokenRepository.getAccessToken().isNotBlank() && tokenRepository.getRefreshToken().isNotBlank() - setState { copy(isUserLoggedIn = isLoggedIn) } return isLoggedIn } From 49d405faee8ff31cc57690c33caa73175021d5ef Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 31 Jul 2025 15:27:54 +0900 Subject: [PATCH 271/299] =?UTF-8?q?[CHORE/#309]=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EC=9D=84=20=EC=84=A0=ED=83=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=EC=9D=98=20?= =?UTF-8?q?=EC=96=B8=EC=96=B4=EB=8C=80=EC=9D=91=EA=B3=BC=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=95=84=EC=9B=83=EC=9D=84=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/component/timepicker/BottomSheetTimePicker.kt | 3 +++ .../component/NotificationSettingTimePicker.kt | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt index 657ad047..d35dcf10 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/component/timepicker/BottomSheetTimePicker.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -104,6 +105,7 @@ fun BottomSheetTimePicker( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { + Spacer(Modifier.weight(1f)) ClodyPicker( state = amPmPickerState, items = amPmLabelItems, @@ -131,6 +133,7 @@ fun BottomSheetTimePicker( modifier = Modifier.weight(1f), textModifier = Modifier.padding(8.dp), ) + Spacer(Modifier.weight(1f)) } } ClodyButton( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt index 145397a1..23c3ecde 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/component/NotificationSettingTimePicker.kt @@ -36,7 +36,8 @@ fun NotificationSettingTimePicker( onDismissRequest: () -> Unit, onConfirm: (TimePeriod, String, String) -> Unit, ) { - val amPmItems = remember { listOf("오전", "오후") } + val amPmItemsLabel = TimePeriod.entries.map { it.getLabel() } + val amPmItems = remember { amPmItemsLabel } val hourItems = remember { (1..12).map { it.toString() } } val minuteItems = remember { listOf("00", "10", "20", "30", "40", "50") } From 45cb1357fa2e988bb6ae980f6cca8b23ba6484b9 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 31 Jul 2025 16:21:05 +0900 Subject: [PATCH 272/299] =?UTF-8?q?[DEL/#309]=20=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=ED=95=A8=EC=88=98=EB=A5=BC=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/utils/extension/StringExt.kt | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt deleted file mode 100644 index ff32b0d7..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/StringExt.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.sopt.clody.presentation.utils.extension - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.sopt.clody.R - -@Composable -fun String.convertTo12HourFormat(): String { - val (hourBefore, minuteBefore) = this.split(":").map { it.toInt() } - - val amPm = if (hourBefore < 12) stringResource(R.string.time_am) else stringResource(R.string.time_pm) - - val hourAfter = when { - hourBefore == 0 -> 12 - hourBefore > 12 -> hourBefore - 12 - else -> hourBefore - } - - val minuteAfter = if (minuteBefore == 0) "00" else minuteBefore.toString() - - return String.format(stringResource(R.string.notification_setting_selected_time, amPm, hourAfter, minuteAfter)) -} - -fun Triple.to24HourFormat(): String { - val (amPm, hour, minute) = this - val hourInt = when { - amPm == "오후" && hour.toInt() != 12 -> hour.toInt() + 12 - amPm == "오전" && hour.toInt() == 12 -> 0 - else -> hour.toInt() - } - return String.format("%02d:%02d", hourInt, minute.toInt()) -} From 90ba7af702e7219327cffedd2f13bbca35f53387 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 31 Jul 2025 16:21:34 +0900 Subject: [PATCH 273/299] =?UTF-8?q?[REFACTOR/#309]=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=EC=97=90=20=EA=B4=80=EB=A0=A8=EB=90=9C=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EB=8B=A4=EA=B5=AD=EC=96=B4=20=EB=8C=80?= =?UTF-8?q?=EC=9D=91=EC=9D=84=20=EC=A7=84=ED=96=89=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/signup/NicknameMessage.kt | 17 +++++++++++++++++ .../ui/auth/signup/SignUpContract.kt | 6 +----- .../ui/auth/signup/SignUpViewModel.kt | 7 +++---- .../ui/auth/signup/page/NicknamePage.kt | 7 ++++--- .../component/NicknameChangeBottomSheet.kt | 7 ++++--- .../setting/screen/AccountManagementScreen.kt | 3 ++- .../screen/AccountManagementViewModel.kt | 14 +++++--------- app/src/main/res/values-ko/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 3 +++ 9 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameMessage.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameMessage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameMessage.kt new file mode 100644 index 00000000..b00de9e2 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/NicknameMessage.kt @@ -0,0 +1,17 @@ +package com.sopt.clody.presentation.ui.auth.signup + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.sopt.clody.R + +enum class NicknameMessage( + @StringRes val message: Int, +) { + DEFAULT(R.string.nickname_message_default), + INVALID(R.string.nickname_message_invalid), + ; + + @Composable + fun getMessage(): String = stringResource(message) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt index 746f35e7..ec6d4d9b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpContract.kt @@ -10,7 +10,7 @@ class SignUpContract { val isNicknameFocused: Boolean = false, val isValidNickname: Boolean = true, val nicknameMaxLength: Int = 15, - val nicknameMessage: String = DEFAULT_NICKNAME_MESSAGE, + val nicknameMessage: NicknameMessage = NicknameMessage.DEFAULT, val isLoading: Boolean = false, val errorMessage: String? = null, val serviceChecked: Boolean = false, @@ -47,8 +47,4 @@ class SignUpContract { data class NavigateToWebView(val url: String) : SignUpSideEffect data class ShowMessage(val message: String) : SignUpSideEffect } - - companion object { - const val DEFAULT_NICKNAME_MESSAGE = "특수문자, 띄어쓰기 없이 작성해주세요" - } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index 0909bcd7..f12f90bf 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -12,7 +12,6 @@ import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository -import com.sopt.clody.presentation.ui.auth.signup.SignUpContract.Companion.DEFAULT_NICKNAME_MESSAGE import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls import com.sopt.clody.presentation.utils.language.LanguageProvider import dagger.assisted.Assisted @@ -76,9 +75,9 @@ class SignUpViewModel @AssistedInject constructor( nickname = intent.value, isValidNickname = isValid, nicknameMessage = if (intent.value.isEmpty() || isValid) { - DEFAULT_NICKNAME_MESSAGE + NicknameMessage.DEFAULT } else { - "사용할 수 없는 닉네임이에요" + NicknameMessage.INVALID }, ) } @@ -134,7 +133,7 @@ class SignUpViewModel @AssistedInject constructor( nickname = "", isNicknameFocused = false, isValidNickname = true, - nicknameMessage = SignUpContract.DEFAULT_NICKNAME_MESSAGE, + nicknameMessage = NicknameMessage.DEFAULT, ) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt index 50a591b9..ec5e22ff 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/NicknamePage.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.presentation.ui.auth.component.textfield.NickNameTextField +import com.sopt.clody.presentation.ui.auth.signup.NicknameMessage import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.utils.base.BasePreview @@ -39,7 +40,7 @@ fun NickNamePage( nickname: String, isValidNickname: Boolean, nicknameMaxLength: Int, - nicknameMessage: String, + nicknameMessage: NicknameMessage, isLoading: Boolean, isFocused: Boolean, onNicknameChange: (String) -> Unit, @@ -119,7 +120,7 @@ fun NickNamePage( horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = nicknameMessage, + text = nicknameMessage.getMessage(), color = when { nickname.isEmpty() -> ClodyTheme.colors.gray04 isValidNickname -> ClodyTheme.colors.gray04 @@ -149,7 +150,7 @@ private fun NicknamePagePreview() { nickname = "클로디", isValidNickname = true, nicknameMaxLength = 15, - nicknameMessage = "사용 가능한 닉네임입니다.", + nicknameMessage = NicknameMessage.DEFAULT, isLoading = false, isFocused = false, onNicknameChange = {}, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt index 201072b5..8488904a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/NicknameChangeBottomSheet.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.request.ModifyNicknameRequestDto +import com.sopt.clody.presentation.ui.auth.signup.NicknameMessage import com.sopt.clody.presentation.ui.component.bottomsheet.ClodyBottomSheet import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.setting.screen.AccountManagementViewModel @@ -38,7 +39,7 @@ fun NicknameChangeBottomSheet( userName: String, isValidNickname: Boolean, nicknameMaxLength: Int, - nicknameMessage: String, + nicknameMessage: NicknameMessage, onDismiss: () -> Unit, ) { ClodyBottomSheet( @@ -62,7 +63,7 @@ fun NicknameChangeBottomSheetItem( userName: String, isValidNickname: Boolean, nicknameMaxLength: Int, - nicknameMessage: String, + nicknameMessage: NicknameMessage, onDismiss: () -> Unit, ) { var nickname by remember { mutableStateOf(TextFieldValue("")) } @@ -134,7 +135,7 @@ fun NicknameChangeBottomSheetItem( .padding(horizontal = 24.dp), ) { Text( - text = nicknameMessage, + text = nicknameMessage.getMessage(), color = when { nickname.text.isEmpty() -> ClodyTheme.colors.gray04 isValidNickname -> ClodyTheme.colors.gray04 diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt index 168dbfbb..79b25403 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R +import com.sopt.clody.presentation.ui.auth.signup.NicknameMessage import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.dialog.ClodyDialog @@ -103,7 +104,7 @@ fun AccountManagementScreen( updateNicknameChangeBottomSheet: (Boolean) -> Unit, isValidNickname: Boolean, nicknameMaxLength: Int, - nicknameMessage: String, + nicknameMessage: NicknameMessage, showLogoutDialog: Boolean, updateLogoutDialog: (Boolean) -> Unit, showRevokeDialog: Boolean, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt index 26efca67..5c374523 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt @@ -7,6 +7,7 @@ import com.sopt.clody.data.datastore.TokenDataStore import com.sopt.clody.data.remote.dto.request.ModifyNicknameRequestDto import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AccountManagementRepository +import com.sopt.clody.presentation.ui.auth.signup.NicknameMessage import com.sopt.clody.presentation.utils.language.LanguageProvider import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE @@ -35,8 +36,8 @@ class AccountManagementViewModel @Inject constructor( private val _isValidNickname = MutableStateFlow(true) val isValidNickname: StateFlow = _isValidNickname - private val _nicknameMessage = MutableStateFlow(DEFAULT_NICKNAME_MESSAGE) - val nicknameMessage: StateFlow = _nicknameMessage + private val _nicknameMessage = MutableStateFlow(NicknameMessage.DEFAULT) + val nicknameMessage: StateFlow = _nicknameMessage private val _logOutState = MutableStateFlow(LogOutState.Idle) val logOutState: StateFlow = _logOutState @@ -114,10 +115,10 @@ class AccountManagementViewModel @Inject constructor( if (nickname.isNotEmpty()) { val isValid = nickname.matches(Regex("^[a-zA-Z가-힣0-9ㄱ-ㅎㅏ-ㅣ가-힣]{2,${_nicknameMaxLength.value}}$")) _isValidNickname.value = isValid - _nicknameMessage.value = if (isValid) DEFAULT_NICKNAME_MESSAGE else FAILURE_NICKNAME_MESSAGE + _nicknameMessage.value = if (isValid) NicknameMessage.DEFAULT else NicknameMessage.INVALID } else { _isValidNickname.value = true - _nicknameMessage.value = DEFAULT_NICKNAME_MESSAGE + _nicknameMessage.value = NicknameMessage.DEFAULT } } @@ -167,9 +168,4 @@ class AccountManagementViewModel @Inject constructor( _showFailureDialog.value = false _failureDialogMessage.value = "" } - - companion object { - private const val DEFAULT_NICKNAME_MESSAGE = "특수문자, 띄어쓰기 없이 작성해주세요" - private const val FAILURE_NICKNAME_MESSAGE = "사용할 수 없는 닉네임이에요" - } } diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index c03143be..bf0e2d55 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -16,6 +16,8 @@ 만나서 반가워요!\n어떻게 불러 드릴까요? 닉네임을 입력해주세요 + 특수문자, 띄어쓰기 없이 작성해주세요 + 사용할 수 없는 닉네임이에요 다음 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b0a8d8d..e026628a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,8 +16,11 @@ Nice to meet you!\nWhat should I call you? Please enter your nickname + Please write without spaces or special characters + This nickname isn\'t available Next + What time would you\nlike us to remind you to write? %2$s:%3$s %1$s From 50db4e445d496799a26612b7b670f8f9079badde Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 1 Aug 2025 16:11:12 +0900 Subject: [PATCH 274/299] =?UTF-8?q?[REFACTOR/#314]=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B0=8F=20=EA=B3=84=EC=A0=95=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=A9=EB=8B=88=EB=8B=A4.=20-?= =?UTF-8?q?=20OAuthProvider=20=ED=81=B4=EB=9E=98=EC=8A=A4=EC=9D=98=20apiVa?= =?UTF-8?q?lue=20=EC=86=8D=EC=84=B1=EB=AA=85=EC=9D=84platform=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EC=97=AC=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=EC=99=80=20=ED=86=B5=EC=9D=BC=ED=95=98=EA=B3=A0,=20?= =?UTF-8?q?=EC=A7=81=EA=B4=80=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.=20-=20UserInfoRe?= =?UTF-8?q?sponseDto=EB=8A=94=20=EC=84=9C=EB=B2=84=EB=A1=9C=20=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EA=B3=BC=20=EC=B5=9C=EB=8C=80=ED=95=9C=20=EC=9C=A0=EC=82=AC?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=B0=9B=EA=B3=A0=20OAuthProvider=20?= =?UTF-8?q?=EB=93=B1=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=A1=9C=20=EB=A7=A4=ED=95=91=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=EC=9D=80=20=EC=B6=94=ED=9B=84=20Domain=20Lay?= =?UTF-8?q?er=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=98=EB=8A=94=EA=B2=8C=20?= =?UTF-8?q?=EC=A2=8B=EC=9D=84=20=EA=B2=83=20=EA=B0=99=EB=8B=A4=20=EC=83=9D?= =?UTF-8?q?=EA=B0=81=EB=90=98=EC=96=B4=20=EC=88=98=EC=A0=95=ED=96=88?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.=20-=20=EC=84=9C=EB=B2=84=EC=99=80?= =?UTF-8?q?=20=ED=98=91=EC=9D=98=20=ED=9B=84=EC=97=90=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20API=EC=9D=98=20404=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=A7=81=ED=95=98=EC=97=AC=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=B6=84=EA=B8=B0=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.=20(500=EC=82=AD?= =?UTF-8?q?=EC=A0=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/data/datastore/OAuthProvider.kt | 2 +- .../dto/response/UserInfoResponseDto.kt | 3 +- .../ui/auth/signup/SignUpViewModel.kt | 4 +-- .../ui/login/GoogleSignInHelper.kt | 29 ++++++++++--------- .../presentation/ui/login/LoginScreen.kt | 4 +-- .../presentation/ui/login/LoginViewModel.kt | 6 ++-- .../AccountManagementLogoutOption.kt | 7 ++--- .../screen/AccountManagementViewModel.kt | 3 +- 8 files changed, 27 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/datastore/OAuthProvider.kt b/app/src/main/java/com/sopt/clody/data/datastore/OAuthProvider.kt index c8c57020..e4151077 100644 --- a/app/src/main/java/com/sopt/clody/data/datastore/OAuthProvider.kt +++ b/app/src/main/java/com/sopt/clody/data/datastore/OAuthProvider.kt @@ -1,6 +1,6 @@ package com.sopt.clody.data.datastore -enum class OAuthProvider(val apiValue: String) { +enum class OAuthProvider(val platform: String) { GOOGLE("google"), KAKAO("kakao"), } diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/UserInfoResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/UserInfoResponseDto.kt index ba4297e9..558e2797 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/UserInfoResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/UserInfoResponseDto.kt @@ -1,6 +1,5 @@ package com.sopt.clody.data.remote.dto.response -import com.sopt.clody.data.datastore.OAuthProvider import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -8,5 +7,5 @@ import kotlinx.serialization.Serializable data class UserInfoResponseDto( @SerialName("email") val email: String, @SerialName("name") val name: String, - @SerialName("platform") val platform: OAuthProvider?, + @SerialName("platform") val platform: String, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index 3ecb7f01..b9ee5e6d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -162,7 +162,7 @@ class SignUpViewModel @AssistedInject constructor( return } val request = SignUpRequestDto( - platform = OAuthProvider.GOOGLE.apiValue, + platform = OAuthProvider.GOOGLE.platform, name = state.nickname, fcmToken = fcmToken, ) @@ -173,7 +173,7 @@ class SignUpViewModel @AssistedInject constructor( loginSdk.login(context).fold( onSuccess = { token -> val request = SignUpRequestDto( - platform = OAuthProvider.KAKAO.apiValue, + platform = OAuthProvider.KAKAO.platform, name = state.nickname, fcmToken = fcmToken, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt index 67dc80f1..a9644701 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/GoogleSignInHelper.kt @@ -1,6 +1,7 @@ package com.sopt.clody.presentation.ui.login import android.content.Context +import android.content.Intent import androidx.activity.result.IntentSenderRequest import com.google.android.gms.auth.api.identity.BeginSignInRequest import com.google.android.gms.auth.api.identity.Identity @@ -11,19 +12,6 @@ class GoogleSignInHelper(context: Context) { private val signInClient: SignInClient = Identity.getSignInClient(context.applicationContext) - fun buildSignInRequest(): BeginSignInRequest { - return BeginSignInRequest.Builder() - .setGoogleIdTokenRequestOptions( - BeginSignInRequest.GoogleIdTokenRequestOptions.builder() - .setSupported(true) - .setServerClientId(BuildConfig.GOOGLE_AUTH_WEB_CLIENT_ID) - .setFilterByAuthorizedAccounts(false) - .build(), - ) - .setAutoSelectEnabled(false) - .build() - } - fun requestSignIn( onSuccess: (IntentSenderRequest) -> Unit, onFailure: (Exception) -> Unit, @@ -40,7 +28,20 @@ class GoogleSignInHelper(context: Context) { } } - fun extractIdToken(data: android.content.Intent?): String? { + private fun buildSignInRequest(): BeginSignInRequest { + return BeginSignInRequest.Builder() + .setGoogleIdTokenRequestOptions( + BeginSignInRequest.GoogleIdTokenRequestOptions.builder() + .setSupported(true) + .setServerClientId(BuildConfig.GOOGLE_AUTH_WEB_CLIENT_ID) + .setFilterByAuthorizedAccounts(false) + .build(), + ) + .setAutoSelectEnabled(false) + .build() + } + + fun extractIdToken(data: Intent?): String? { return runCatching { val credential = signInClient.getSignInCredentialFromIntent(data) credential.googleIdToken diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt index 476e81be..783962b2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginScreen.kt @@ -83,9 +83,7 @@ fun LoginRoute( onGoogleLoginClick = { googleSignInHelper.requestSignIn( onSuccess = { intentSenderRequest -> googleSignInLauncher.launch(intentSenderRequest) }, - onFailure = { - viewModel.postIntent(LoginIntent.ClearError) - }, + onFailure = { viewModel.postIntent(LoginIntent.ClearError) }, ) }, ) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt index e5d53217..498fa34b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -87,7 +87,7 @@ class LoginViewModel @AssistedInject constructor( private suspend fun validateKakaoUser(kakaoToken: String) { val fcmToken = fcmTokenProvider.getToken().orEmpty() - val request = LoginRequestDto(platform = OAuthProvider.KAKAO.apiValue, fcmToken = fcmToken) + val request = LoginRequestDto(platform = OAuthProvider.KAKAO.platform, fcmToken = fcmToken) authRepository.signIn("Bearer $kakaoToken", request).fold( onSuccess = { @@ -98,7 +98,7 @@ class LoginViewModel @AssistedInject constructor( onFailure = { error -> setState { copy(isLoading = false) } val msg = error.message.orEmpty() - if (msg.contains("404") || msg.contains("유저가 없습니다")) { + if (msg.contains("404")) { _sideEffects.send(LoginContract.LoginSideEffect.NavigateToSignUp) } else { _sideEffects.send(LoginContract.LoginSideEffect.ShowError(msg)) @@ -124,7 +124,7 @@ class LoginViewModel @AssistedInject constructor( setState { copy(isLoading = false) } val msg = error.message.orEmpty() - if (msg.contains("500") || msg.contains("유저가 없습니다")) { + if (msg.contains("404")) { oauthDataStore.saveIdToken(idToken) oauthDataStore.savePlatform(OAuthProvider.GOOGLE) _sideEffects.send(LoginContract.LoginSideEffect.NavigateToSignUp) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt index 435d7fcf..f171c547 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/AccountManagementLogoutOption.kt @@ -16,18 +16,17 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sopt.clody.R -import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.ui.theme.ClodyTheme @Composable fun AccountManagementLogoutOption( userEmail: String, - platform: OAuthProvider?, + platform: String, updateLogoutDialog: (Boolean) -> Unit, ) { val platformIconRes = when (platform) { - OAuthProvider.KAKAO -> R.drawable.img_account_management_kakao - OAuthProvider.GOOGLE -> R.drawable.img_google_button_logo + "kakao" -> R.drawable.img_account_management_kakao + "google" -> R.drawable.img_google_button_logo else -> R.drawable.img_google_button_logo // 서버에서 google을 어떻게 내려줄까요? } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt index 976d9893..5c374523 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt @@ -68,9 +68,8 @@ class AccountManagementViewModel @Inject constructor( val result = accountManagementRepository.getUserInfo() _userInfoState.value = result.fold( onSuccess = { - val platformEnum = it.platform retryCount = 0 - UserInfoState.Success(it.copy(platform = platformEnum)) + UserInfoState.Success(it) }, onFailure = { retryCount++ From a066b7eea9adfa3a0fa670e09c5b24b538c04665 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 2 Aug 2025 01:22:00 +0900 Subject: [PATCH 275/299] =?UTF-8?q?[REFACTOR/#314]=20=EC=9B=B9=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=A1=9C=EB=93=9C=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9D=84=20=EC=9B=B9=EB=B7=B0=EC=97=90=EC=84=9C=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20=EC=95=B1=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 2 - .../sopt/clody/core/login/KakaoLoginSdk.kt | 2 +- .../DefaultLoginSecurityChecker.kt | 2 +- .../{login => }/LoginSecurityChecker.kt | 2 +- .../security/{login => }/SecurityModule.kt | 2 +- .../security/weview/SecureWebViewClient.kt | 33 -------- .../ui/auth/signup/SignUpScreen.kt | 6 +- .../signup/navigation/SignUpNavigation.kt | 2 - .../presentation/ui/main/ClodyNavHost.kt | 7 -- .../setting/navigation/SettingNavigation.kt | 9 -- .../ui/setting/screen/SettingScreen.kt | 12 +-- .../ui/webview/WebViewNavGraph.kt | 19 ----- .../presentation/ui/webview/WebViewScreen.kt | 83 ------------------- .../presentation/utils/OpenExternalBrowser.kt | 17 ++++ .../presentation/utils/navigation/Route.kt | 3 - 15 files changed, 30 insertions(+), 171 deletions(-) rename app/src/main/java/com/sopt/clody/core/security/{login => }/DefaultLoginSecurityChecker.kt (97%) rename app/src/main/java/com/sopt/clody/core/security/{login => }/LoginSecurityChecker.kt (78%) rename app/src/main/java/com/sopt/clody/core/security/{login => }/SecurityModule.kt (89%) delete mode 100644 app/src/main/java/com/sopt/clody/core/security/weview/SecureWebViewClient.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewNavGraph.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewScreen.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/OpenExternalBrowser.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c8bfb09f..fd21eb50 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,14 +32,12 @@ android { val googleAdmobAppId: String = properties.getProperty("GOOGLE_ADMOB_APP_ID", "") val googleAdmobUnitId: String = properties.getProperty("GOOGLE_ADMOB_UNIT_ID", "") val googleAuthWebClientId: String = properties.getProperty("GOOGLE_AUTH_WEB_CLIENT_ID", "") - val allowedDomains: String = properties.getProperty("allowed.webview.domains", "notion.so,google.com") buildConfigField("String", "GOOGLE_ADMOB_APP_ID", "\"$googleAdmobAppId\"") buildConfigField("String", "GOOGLE_ADMOB_UNIT_ID", "\"$googleAdmobUnitId\"") buildConfigField("String", "KAKAO_API_KEY", "\"$kakaoApiKey\"") buildConfigField("String", "AMPLITUDE_API_KEY", "\"$amplitudeApiKey\"") buildConfigField("String", "GOOGLE_AUTH_WEB_CLIENT_ID", "\"$googleAuthWebClientId\"") - buildConfigField("String", "ALLOWED_WEBVIEW_DOMAINS", "\"$allowedDomains\"") manifestPlaceholders["kakaoRedirectUri"] = "kakao$kakaoApiKey" manifestPlaceholders["GOOGLE_ADMOB_APP_ID"] = googleAdmobAppId testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt b/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt index d24a3e95..8abc67bd 100644 --- a/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt +++ b/app/src/main/java/com/sopt/clody/core/login/KakaoLoginSdk.kt @@ -7,7 +7,7 @@ import com.kakao.sdk.common.model.ClientError import com.kakao.sdk.common.model.ClientErrorCause import com.kakao.sdk.user.UserApiClient import com.sopt.clody.R -import com.sopt.clody.core.security.login.LoginSecurityChecker +import com.sopt.clody.core.security.LoginSecurityChecker import kotlinx.coroutines.suspendCancellableCoroutine import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt b/app/src/main/java/com/sopt/clody/core/security/DefaultLoginSecurityChecker.kt similarity index 97% rename from app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt rename to app/src/main/java/com/sopt/clody/core/security/DefaultLoginSecurityChecker.kt index 400c45d0..21f4e87f 100644 --- a/app/src/main/java/com/sopt/clody/core/security/login/DefaultLoginSecurityChecker.kt +++ b/app/src/main/java/com/sopt/clody/core/security/DefaultLoginSecurityChecker.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.core.security.login +package com.sopt.clody.core.security import android.content.Context import android.content.pm.PackageManager diff --git a/app/src/main/java/com/sopt/clody/core/security/login/LoginSecurityChecker.kt b/app/src/main/java/com/sopt/clody/core/security/LoginSecurityChecker.kt similarity index 78% rename from app/src/main/java/com/sopt/clody/core/security/login/LoginSecurityChecker.kt rename to app/src/main/java/com/sopt/clody/core/security/LoginSecurityChecker.kt index 4e0b06e3..023bff55 100644 --- a/app/src/main/java/com/sopt/clody/core/security/login/LoginSecurityChecker.kt +++ b/app/src/main/java/com/sopt/clody/core/security/LoginSecurityChecker.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.core.security.login +package com.sopt.clody.core.security import android.content.Context diff --git a/app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt b/app/src/main/java/com/sopt/clody/core/security/SecurityModule.kt similarity index 89% rename from app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt rename to app/src/main/java/com/sopt/clody/core/security/SecurityModule.kt index e20c1e10..0f514829 100644 --- a/app/src/main/java/com/sopt/clody/core/security/login/SecurityModule.kt +++ b/app/src/main/java/com/sopt/clody/core/security/SecurityModule.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.core.security.login +package com.sopt.clody.core.security import dagger.Binds import dagger.Module diff --git a/app/src/main/java/com/sopt/clody/core/security/weview/SecureWebViewClient.kt b/app/src/main/java/com/sopt/clody/core/security/weview/SecureWebViewClient.kt deleted file mode 100644 index 44bd523b..00000000 --- a/app/src/main/java/com/sopt/clody/core/security/weview/SecureWebViewClient.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.sopt.clody.core.security.weview - -import android.content.Context -import android.content.Intent -import android.net.http.SslError -import android.webkit.SslErrorHandler -import android.webkit.WebResourceRequest -import android.webkit.WebView -import android.webkit.WebViewClient - -class SecureWebViewClient( - private val context: Context, - private val allowedDomains: List = listOf("notion.so", "forms.gle"), -) : WebViewClient() { - - override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { - val host = request.url.host ?: return true - val isSafeDomain = allowedDomains.any { host.contains(it) } - - return if (isSafeDomain) { - false - } else { - Intent(Intent.ACTION_VIEW, request.url).let { - context.startActivity(it) - } - true - } - } - - override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) { - handler.cancel() - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt index 160e48f2..3937e5d7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpScreen.kt @@ -12,13 +12,13 @@ import com.sopt.clody.presentation.ui.auth.signup.page.NickNamePage import com.sopt.clody.presentation.ui.auth.signup.page.TermsOfServicePage import com.sopt.clody.presentation.ui.component.dialog.FailureDialog import com.sopt.clody.presentation.utils.extension.repeatOnStarted +import com.sopt.clody.presentation.utils.openExternalBrowser @Composable fun SignUpRoute( viewModel: SignUpViewModel = mavericksViewModel(), navigateToHome: () -> Unit, navigateToPrevious: () -> Unit, - navigateToWebView: (String) -> Unit, ) { val state by viewModel.collectAsState() val context = LocalContext.current @@ -29,9 +29,7 @@ fun SignUpRoute( viewModel.sideEffects.collect { effect -> when (effect) { is SignUpContract.SignUpSideEffect.NavigateToTimeReminder -> navigateToHome() - is SignUpContract.SignUpSideEffect.NavigateToWebView -> { - navigateToWebView(effect.url) // ✅ WebView 이동 처리 - } + is SignUpContract.SignUpSideEffect.NavigateToWebView -> { openExternalBrowser(context, effect.url) } is SignUpContract.SignUpSideEffect.ShowMessage -> {} } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt index 42ea17b8..9c7a7de6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/navigation/SignUpNavigation.kt @@ -10,13 +10,11 @@ import com.sopt.clody.presentation.utils.navigation.Route fun NavGraphBuilder.signUpScreen( navigateToHome: () -> Unit, navigateToPrevious: () -> Unit, - navigateToWebView: (String) -> Unit, ) { composable { SignUpRoute( navigateToHome = navigateToHome, navigateToPrevious = navigateToPrevious, - navigateToWebView = navigateToWebView, ) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt index b8fcbb80..8408e01d 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/main/ClodyNavHost.kt @@ -27,11 +27,9 @@ import com.sopt.clody.presentation.ui.setting.navigation.accountManagementScreen import com.sopt.clody.presentation.ui.setting.navigation.navigateToAccountManagement import com.sopt.clody.presentation.ui.setting.navigation.navigateToNotificationSetting import com.sopt.clody.presentation.ui.setting.navigation.navigateToSetting -import com.sopt.clody.presentation.ui.setting.navigation.navigateToWebView import com.sopt.clody.presentation.ui.setting.navigation.notificationSettingScreen import com.sopt.clody.presentation.ui.setting.navigation.settingScreen import com.sopt.clody.presentation.ui.splash.navigation.splashScreen -import com.sopt.clody.presentation.ui.webview.webViewScreen import com.sopt.clody.presentation.ui.writediary.navigation.navigateToWriteDiary import com.sopt.clody.presentation.ui.writediary.navigation.writeDiaryScreen import com.sopt.clody.presentation.utils.navigation.safePopBackStack @@ -73,7 +71,6 @@ fun ClodyNavHost( signUpScreen( navigateToHome = navController::navigateToTimeReminder, navigateToPrevious = navController::safePopBackStack, - navigateToWebView = navController::navigateToWebView, ) timeReminderScreen( navigateToGuide = navController::navigateToGuide, @@ -108,7 +105,6 @@ fun ClodyNavHost( navigateToAccountManagement = navController::navigateToAccountManagement, navigateToNotification = navController::navigateToNotificationSetting, navigateToPrevious = navController::safePopBackStack, - navigateToWebView = navController::navigateToWebView, ) accountManagementScreen( navigateToPrevious = navController::safePopBackStack, @@ -117,9 +113,6 @@ fun ClodyNavHost( notificationSettingScreen( navigateToPrevious = navController::safePopBackStack, ) - webViewScreen( - navigateToPrevious = navController::safePopBackStack, - ) } } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt index 572e088c..b2fc48f4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/navigation/SettingNavigation.kt @@ -13,14 +13,12 @@ fun NavGraphBuilder.settingScreen( navigateToAccountManagement: () -> Unit, navigateToNotification: () -> Unit, navigateToPrevious: () -> Unit, - navigateToWebView: (String) -> Unit, ) { composable { SettingRoute( navigateToAccountManagement = navigateToAccountManagement, navigateToNotification = navigateToNotification, navigateToPrevious = navigateToPrevious, - navigateToWebView = navigateToWebView, ) } } @@ -62,10 +60,3 @@ fun NavController.navigateToNotificationSetting( ) { navigate(Route.NotificationSetting, navOptions) } - -fun NavController.navigateToWebView( - encodedUrl: String, - navOptions: NavOptionsBuilder.() -> Unit = {}, -) { - navigate(Route.WebView(encodedUrl), navOptions) -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt index 291bed09..07b12378 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R @@ -19,6 +20,7 @@ import com.sopt.clody.presentation.ui.setting.component.SettingTopAppBar import com.sopt.clody.presentation.ui.setting.component.SettingVersionInfo import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.openExternalBrowser import com.sopt.clody.ui.theme.ClodyTheme @Composable @@ -26,7 +28,6 @@ fun SettingRoute( navigateToAccountManagement: () -> Unit, navigateToNotification: () -> Unit, navigateToPrevious: () -> Unit, - navigateToWebView: (String) -> Unit, settingViewModel: SettingViewModel = hiltViewModel(), ) { val notice by settingViewModel::noticeUrl @@ -34,6 +35,7 @@ fun SettingRoute( val termsOfService by settingViewModel::termsOfServiceUrl val privacyPolicy by settingViewModel::privacyPolicyUrl val versionInfo by settingViewModel::versionInfo + val context = LocalContext.current LaunchedEffect(Unit) { settingViewModel.getVersionInfo() @@ -45,10 +47,10 @@ fun SettingRoute( onClickBack = navigateToPrevious, onClickAccountManagement = navigateToAccountManagement, onClickNotificationSetting = navigateToNotification, - onClickNotice = { navigateToWebView(notice) }, - onClickSupportFeedback = { navigateToWebView(supportFeedback) }, - onClickTerms = { navigateToWebView(termsOfService) }, - onClickPrivacy = { navigateToWebView(privacyPolicy) }, + onClickNotice = { openExternalBrowser(context, notice) }, + onClickSupportFeedback = { openExternalBrowser(context, supportFeedback) }, + onClickTerms = { openExternalBrowser(context, termsOfService) }, + onClickPrivacy = { openExternalBrowser(context, privacyPolicy) }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewNavGraph.kt b/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewNavGraph.kt deleted file mode 100644 index 8c20e749..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewNavGraph.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.sopt.clody.presentation.ui.webview - -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import androidx.navigation.toRoute -import com.sopt.clody.presentation.utils.navigation.Route - -fun NavGraphBuilder.webViewScreen( - navigateToPrevious: () -> Unit, -) { - composable { backStackEntry -> - backStackEntry.toRoute().apply { - WebViewRoute( - encodedUrl = encodedUrl, - navigateToPrevious = navigateToPrevious, - ) - } - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewScreen.kt deleted file mode 100644 index 0515fc59..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/webview/WebViewScreen.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.sopt.clody.presentation.ui.webview - -import android.annotation.SuppressLint -import android.net.Uri -import android.webkit.WebSettings -import android.webkit.WebView -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import com.sopt.clody.BuildConfig -import com.sopt.clody.core.security.weview.SecureWebViewClient - -@Composable -fun WebViewRoute( - navigateToPrevious: () -> Unit, - encodedUrl: String, -) { - WebViewScreen( - encodedUrl = encodedUrl, - onClickBack = navigateToPrevious, - ) -} - -@SuppressLint("SetJavaScriptEnabled") -@Composable -fun WebViewScreen( - encodedUrl: String, - onClickBack: () -> Unit, -) { - val decodedUrl = remember(encodedUrl) { - Uri.decode(encodedUrl) - } - - var webView: WebView? by remember { mutableStateOf(null) } - val canGoBack by remember { derivedStateOf { webView?.canGoBack() ?: false } } - - val allowedDomains = BuildConfig.ALLOWED_WEBVIEW_DOMAINS.split(",").map { it.trim() } - - Scaffold( - modifier = Modifier.fillMaxSize(), - content = { innerPadding -> - AndroidView( - factory = { context -> - WebView(context).apply { - webViewClient = SecureWebViewClient(context, allowedDomains) - settings.apply { - javaScriptEnabled = true - domStorageEnabled = true - useWideViewPort = true - loadWithOverviewMode = true - allowFileAccess = false - allowContentAccess = false - javaScriptCanOpenWindowsAutomatically = false - mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW - } - loadUrl(decodedUrl) - webView = this - } - }, - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), - ) - }, - ) - - BackHandler(enabled = canGoBack) { - if (canGoBack) { - webView?.goBack() - } else { - onClickBack() - } - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/OpenExternalBrowser.kt b/app/src/main/java/com/sopt/clody/presentation/utils/OpenExternalBrowser.kt new file mode 100644 index 00000000..aaab7895 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/OpenExternalBrowser.kt @@ -0,0 +1,17 @@ +package com.sopt.clody.presentation.utils + +import android.content.Context +import android.content.Intent +import android.net.Uri + +fun openExternalBrowser(context: Context, url: String) { + val uri = Uri.parse(url) + val intent = Intent(Intent.ACTION_VIEW, uri).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + + // 웹 브라우저 앱이 설치되어 있지 않은 경우 + context.packageManager.resolveActivity(intent, 0)?.let { + context.startActivity(intent) + } ?: return +} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt index 334475b6..d00a7cff 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt @@ -78,7 +78,4 @@ sealed interface Route { @Serializable data object NotificationSetting : Route - - @Serializable - data class WebView(val encodedUrl: String) : Route } From d8b5dd3a0456c577e4a73985dabc68781be05073 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 2 Aug 2025 02:12:17 +0900 Subject: [PATCH 276/299] =?UTF-8?q?[FEAT/#314]=20=EC=95=B1=EC=9D=B4=20?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=20=EB=B2=84=EC=A0=84=EC=9D=BC=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EC=B5=9C=EC=8B=A0=20=EB=B2=84=EC=A0=84=EC=9D=B4?= =?UTF-8?q?=EB=9D=BC=EA=B3=A0=20=ED=91=9C=EC=8B=9C=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EA=B5=AC=ED=98=84=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/setting/component/SettingVersionInfo.kt | 5 +---- .../ui/setting/screen/SettingScreen.kt | 1 - .../ui/setting/screen/SettingViewModel.kt | 16 +++++++++++++--- app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/SettingVersionInfo.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/SettingVersionInfo.kt index fcb068ea..84649dd9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/SettingVersionInfo.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/component/SettingVersionInfo.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.sopt.clody.R import com.sopt.clody.ui.theme.ClodyTheme @@ -34,9 +33,7 @@ fun SettingVersionInfo( Text( text = versionInfo, color = ClodyTheme.colors.gray05, - style = ClodyTheme.typography.body4Medium.copy( - letterSpacing = 2.sp, - ), + style = ClodyTheme.typography.body4Medium, ) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt index 07b12378..8af5e2c6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingScreen.kt @@ -38,7 +38,6 @@ fun SettingRoute( val context = LocalContext.current LaunchedEffect(Unit) { - settingViewModel.getVersionInfo() AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.SETTING) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingViewModel.kt index e24e922e..d379ed02 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/SettingViewModel.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.R +import com.sopt.clody.data.remote.datasource.RemoteConfigDataSource import com.sopt.clody.presentation.utils.language.LanguageProvider import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -17,23 +18,32 @@ import javax.inject.Inject @HiltViewModel class SettingViewModel @Inject constructor( @ApplicationContext private val context: Context, + private val remoteConfigDataSource: RemoteConfigDataSource, private val languageProvider: LanguageProvider, ) : ViewModel() { val noticeUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.NOTICES_URL) val supportFeedbackUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.SUPPORT_FEEDBACK_URL) val termsOfServiceUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.TERMS_OF_SERVICE_URL) val privacyPolicyUrl = languageProvider.getWebViewUrlFor(SettingOptionUrls.PRIVACY_POLICY_URL) - var versionInfo by mutableStateOf(null) private set - fun getVersionInfo() { + init { + getVersionInfo() + } + + private fun getVersionInfo() { viewModelScope.launch { runCatching { val info: PackageInfo = context.packageManager.getPackageInfo(context.packageName, 0) info.versionName }.onSuccess { versionName -> - versionInfo = versionName + val latestVersion = remoteConfigDataSource.getLatestVersion() + versionInfo = if (versionName == latestVersion) { + context.getString(R.string.setting_option_app_version_info_latest) + } else { + versionName + } }.onFailure { versionInfo = context.getString(R.string.setting_option_app_version_info_failure) } diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bf0e2d55..dc4a25c8 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -91,6 +91,7 @@ 서비스 이용 약관 개인정보 처리방침 앱 버전 + 최신 버전 버전 정보를 불러오는데 실패했습니다. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e026628a..9aea0081 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,6 +92,7 @@ Terms of Service Privacy Policy Version + Latest version Fail to Fetch From f3764ffa253817e326dfe8653117fb541279de70 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 2 Aug 2025 12:05:07 +0900 Subject: [PATCH 277/299] =?UTF-8?q?[CHORE/#314]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95=20=EC=99=84=EB=A3=8C=20?= =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=EC=9D=98=20=EC=9C=84=EC=B9=98=EB=A5=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notificationsetting/screen/NotificationSettingScreen.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index 064424fd..ce2542af 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable @@ -92,6 +93,7 @@ fun NotificationSettingRoute( contentColor = ClodyTheme.colors.white, durationMillis = 3000, onDismiss = { notificationSettingViewModel.resetNotificationTimeChangeState() }, + modifier = Modifier.navigationBarsPadding(), ) } } From 8261e67dbce1661ebb517603866ffde2e3e46296 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sat, 2 Aug 2025 17:38:00 +0900 Subject: [PATCH 278/299] =?UTF-8?q?[CHORE/#314]=20=EA=B0=80=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EC=8A=A4=ED=81=AC=EB=A6=B0=EC=9D=98=20UI=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/auth/guide/GuideScreen.kt | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt index 02422827..ddaa52c5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/guide/GuideScreen.kt @@ -5,13 +5,13 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -38,7 +38,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.button.ClodyButton -import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -111,18 +110,16 @@ fun GuideScreen( AnimatedVisibility( visible = !isExiting, exit = fadeOut(animationSpec = tween(1000)), // 1초 페이드 아웃 - modifier = Modifier - .fillMaxSize() - .padding(innerPadding), + modifier = Modifier.padding(innerPadding), ) { Column( - modifier = Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(modifier = Modifier.heightForScreenPercentage(0.21f)) + Spacer(modifier = Modifier.weight(1f)) HorizontalPager( state = pagerState, + modifier = Modifier.weight(4f), ) { page -> Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -134,14 +131,14 @@ fun GuideScreen( color = ClodyTheme.colors.gray01, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.heightForScreenPercentage(0.02f)) + Spacer(modifier = Modifier.height(20.dp)) Text( text = pages[page].description, style = ClodyTheme.typography.body1Medium, color = ClodyTheme.colors.gray05, textAlign = TextAlign.Center, ) - Spacer(modifier = Modifier.heightForScreenPercentage(0.04f)) + Spacer(modifier = Modifier.weight(1f)) Image( painter = painterResource(id = pages[page].imageRes), contentDescription = null, @@ -149,12 +146,11 @@ fun GuideScreen( .fillMaxWidth(), contentScale = ContentScale.Fit, ) + Spacer(modifier = Modifier.weight(1f)) } } - - Spacer(modifier = Modifier.heightForScreenPercentage(0.2f)) Row( - horizontalArrangement = Arrangement.Center, + modifier = Modifier.padding(vertical = 50.dp), ) { repeat(pagerState.pageCount) { iteration -> val color = if (pagerState.currentPage == iteration) ClodyTheme.colors.gray03 else ClodyTheme.colors.gray07 From 24ed3dfd4ca65dc7dcbd0911edfe7394f0310987 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sun, 3 Aug 2025 17:55:12 +0900 Subject: [PATCH 279/299] =?UTF-8?q?[CHORE/#314]=20=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20API=20=EB=B3=80=EA=B2=BD=EC=82=AC=ED=95=AD?= =?UTF-8?q?=EC=9D=84=20=EB=B0=98=EC=98=81=ED=96=88=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.(data=20=ED=8F=AC=EB=A7=B7=20=EB=B3=80=EA=B2=BD,=20la?= =?UTF-8?q?ng=20=ED=97=A4=EB=8D=94=20=EC=B6=94=EA=B0=80)=20-=20LanguagePro?= =?UTF-8?q?viderImpl=EC=97=90=EC=84=9C=20locale=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9D=84=20=EC=A1=B0=EA=B8=88=20=EB=8D=94=20=EA=B0=80=EB=B3=8D?= =?UTF-8?q?=EA=B2=8C=20=EA=B0=9C=EC=84=A0=ED=96=88=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.=20=EA=B8=B0=EC=A1=B4=20=EB=B0=A9=EC=8B=9D=EC=9D=80?= =?UTF-8?q?=20=EC=95=B1=20=EB=82=B4=20=EB=8B=A4=EA=B5=AD=EC=96=B4=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90=20=EA=B8=B0=EB=8A=A5=EC=9D=B4=20=EC=9E=88?= =?UTF-8?q?=EC=9D=84=EB=95=8C=20=ED=9A=A8=EA=B3=BC=EC=A0=81=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=A9=EB=B2=95=EC=9D=B4=EB=9D=BC=EA=B3=A0=20=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/data/remote/api/DiaryService.kt | 2 ++ .../datasource/DiaryRemoteDataSource.kt | 2 +- .../DiaryRemoteDataSourceImpl.kt | 4 +-- .../dto/request/WriteDiaryRequestDto.kt | 2 +- .../repositoryimpl/DiaryRepositoryImpl.kt | 4 +-- .../domain/repository/DiaryRepository.kt | 2 +- .../writediary/screen/WriteDiaryViewModel.kt | 6 ++-- .../utils/extension/TimeZoneExt.kt | 26 ++++++++++++-- .../utils/language/LanguageProvider.kt | 1 + .../utils/language/LanguageProviderImpl.kt | 35 +++++++------------ 10 files changed, 51 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt b/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt index bd8981d7..62540db6 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/api/DiaryService.kt @@ -13,12 +13,14 @@ import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.POST import retrofit2.http.Query interface DiaryService { @POST("api/v1/diary") suspend fun writeDiary( + @Header("Accept-Language") lang: String, @Body writeDiaryRequestDto: WriteDiaryRequestDto, ): ApiResponse diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt index 26c0d844..be351600 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt @@ -11,7 +11,7 @@ import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto interface DiaryRemoteDataSource { - suspend fun writeDiary(date: String, content: List): ApiResponse + suspend fun writeDiary(lang: String, date: String, content: List): ApiResponse suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): ApiResponse suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): ApiResponse suspend fun getDiaryTime(year: Int, month: Int, date: Int): ApiResponse diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt index f5e35d2a..4522e637 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt @@ -17,8 +17,8 @@ import javax.inject.Inject class DiaryRemoteDataSourceImpl @Inject constructor( private val diaryService: DiaryService, ) : DiaryRemoteDataSource { - override suspend fun writeDiary(date: String, content: List): ApiResponse = - diaryService.writeDiary(WriteDiaryRequestDto(date, content)) + override suspend fun writeDiary(lang: String, date: String, content: List): ApiResponse = + diaryService.writeDiary(lang, WriteDiaryRequestDto(date, content)) override suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): ApiResponse = diaryService.deleteDailyDiary(year = year, month = month, date = date) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/request/WriteDiaryRequestDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/request/WriteDiaryRequestDto.kt index 7026db09..e813934e 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/request/WriteDiaryRequestDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/request/WriteDiaryRequestDto.kt @@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable @Serializable data class WriteDiaryRequestDto( @SerialName("date") val date: String, - @SerialName("content")val content: List, + @SerialName("content") val content: List, ) diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt index febe3024..f09d9787 100644 --- a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt @@ -19,9 +19,9 @@ import javax.inject.Inject class DiaryRepositoryImpl @Inject constructor( private val diaryRemoteDataSource: DiaryRemoteDataSource, ) : DiaryRepository { - override suspend fun writeDiary(date: String, content: List): Result = + override suspend fun writeDiary(lang: String, date: String, content: List): Result = runCatching { - diaryRemoteDataSource.writeDiary(date, content).handleApiResponse().getOrThrow() + diaryRemoteDataSource.writeDiary(lang, date, content).handleApiResponse().getOrThrow() } override suspend fun deleteDailyDiary(year: Int, month: Int, day: Int): Result = diff --git a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt index 3666ed18..0ab3ff7f 100644 --- a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt +++ b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt @@ -9,7 +9,7 @@ import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto import com.sopt.clody.domain.model.DraftDiaryContents interface DiaryRepository { - suspend fun writeDiary(date: String, content: List): Result + suspend fun writeDiary(lang: String, date: String, content: List): Result suspend fun deleteDailyDiary(year: Int, month: Int, day: Int): Result suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): Result suspend fun getDiaryTime(year: Int, month: Int, date: Int): Result diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index 61c133cc..b5a567ec 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -12,6 +12,7 @@ import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.usecase.FetchDraftDiaryUseCase import com.sopt.clody.domain.usecase.SaveDraftDiaryUseCase +import com.sopt.clody.presentation.utils.extension.convertDateToKstDateTime import com.sopt.clody.presentation.utils.language.LanguageProvider import com.sopt.clody.presentation.utils.network.ErrorMessages import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE @@ -81,8 +82,9 @@ class WriteDiaryViewModel @Inject constructor( } _writeDiaryState.value = WriteDiaryState.Loading - val date = String.format("%04d-%02d-%02d", year, month, day) - val result = diaryRepository.writeDiary(date, contents) + val lang = languageProvider.getCurrentLanguageTag() + val date = convertDateToKstDateTime(year, month, day) + val result = diaryRepository.writeDiary(lang, date, contents) _writeDiaryState.value = result.fold( onSuccess = { response -> if (isDiaryExpired(year, month, day)) { diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt index b3b6832d..4314fdb4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt @@ -7,8 +7,8 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter /** -* @param time 서버로부터 수신받은 시간으로 "21:30" 와 같은 형태로 전달받는다. -* */ + * @param time 서버로부터 수신받은 시간으로 "21:30" 와 같은 형태로 전달받는다. + * */ fun convertKSTtoUTZ(time: String, referenceDate: LocalDate = LocalDate.now()): Triple { val kstZoneId = ZoneId.of("Asia/Seoul") val userZoneId = ZoneId.systemDefault() @@ -58,3 +58,25 @@ fun convertUTZtoKST(timePeriod: TimePeriod, hour: String, minute: String, refere return String.format(java.util.Locale.ROOT, "%02d:%02d", kstHour, kstMinute) } + +/** + * 일기작성 API 호출 시 유저의 현재 시점(연/월/일/시각)을 KST 시간대로 변환 후 "yyyy-MM-dd'T'HH:mm:ss" 형식으로 전달한다. + * @param year 작성된 일기의 연도 + * @param month 작성된 일기의 월 + * @param day 작성된 일기의 일 + * */ +fun convertDateToKstDateTime(year: Int, month: Int, day: Int): String { + val localNowDate = LocalDate.now() + val targetDate = LocalDate.of(year, month, day) + val kstZone = ZoneId.of("Asia/Seoul") + val nowKst = ZonedDateTime.now(kstZone) + + val targetZonedDateTime = if (targetDate == localNowDate) { + nowKst + } else { + nowKst.minusDays(1) + } + + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") + return targetZonedDateTime.format(formatter) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt index 4c697c74..57a7889a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProvider.kt @@ -4,6 +4,7 @@ import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls interface LanguageProvider { + fun getCurrentLanguageTag(): String fun getLoginType(): OAuthProvider fun getNicknameMaxLength(): Int fun getDiaryMaxLength(): Int diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt index 6bf0749f..933ecc97 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/language/LanguageProviderImpl.kt @@ -1,38 +1,29 @@ package com.sopt.clody.presentation.utils.language -import android.content.Context import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls -import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Locale import javax.inject.Inject -class LanguageProviderImpl @Inject constructor( - @ApplicationContext private val context: Context, -) : LanguageProvider { - private val locale: Locale - get() = context.resources.configuration.locales[0] +class LanguageProviderImpl @Inject constructor() : LanguageProvider { + private val locale = Locale.getDefault() private fun isKorean(): Boolean = locale.language == LANGUAGE_KO - override fun getLoginType(): OAuthProvider { - return when (locale.language) { - LANGUAGE_KO -> OAuthProvider.KAKAO - else -> OAuthProvider.GOOGLE - } - } + override fun getCurrentLanguageTag(): String = + locale.toLanguageTag() // e.g., "ko-KR" or "en-US" - override fun getNicknameMaxLength(): Int { - return if (isKorean()) NICKNAME_MAX_LENGTH_KO else NICKNAME_MAX_LENGTH_EN - } + override fun getLoginType(): OAuthProvider = + if (isKorean()) OAuthProvider.KAKAO else OAuthProvider.GOOGLE - override fun getDiaryMaxLength(): Int { - return if (isKorean()) DIARY_MAX_LENGTH_KO else DIARY_MAX_LENGTH_EN - } + override fun getNicknameMaxLength(): Int = + if (isKorean()) NICKNAME_MAX_LENGTH_KO else NICKNAME_MAX_LENGTH_EN - override fun getWebViewUrlFor(option: SettingOptionUrls): String { - return if (isKorean()) option.koUrl else option.enUrl - } + override fun getDiaryMaxLength(): Int = + if (isKorean()) DIARY_MAX_LENGTH_KO else DIARY_MAX_LENGTH_EN + + override fun getWebViewUrlFor(option: SettingOptionUrls): String = + if (isKorean()) option.koUrl else option.enUrl companion object { const val LANGUAGE_KO = "ko" From 54564bb4c769f10d47a6324be5496508bda2526a Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sun, 3 Aug 2025 18:35:06 +0900 Subject: [PATCH 280/299] =?UTF-8?q?[REFACTOR/#314]=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20sdk=20=ED=98=B8=EC=B6=9C=EC=9D=B4=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8,=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=8B=9C=202=EB=B2=88=20=ED=98=B8=EC=B6=9C=EB=90=98=EB=8A=94?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=88=98=EC=A0=95=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20-=20=EA=B8=B0=EC=A1=B4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=80=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=99=84=EB=A3=8C=20=ED=9B=84=20=EB=8B=A4=EC=8B=9C?= =?UTF-8?q?=20=ED=95=9C=EB=B2=88=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20sdk?= =?UTF-8?q?=EB=A5=BC=20=ED=98=B8=EC=B6=9C=ED=95=98=EC=97=AC=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=EC=9D=84=20=EB=B0=9B=EC=95=84=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20api=EC=97=90=20=EC=82=AC=EC=9A=A9=ED=96=88?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.=20-=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=EC=97=90=EC=84=9C=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20sdk=20=ED=98=B8=EC=B6=9C=20=ED=9B=84=20OAuthDataSto?= =?UTF-8?q?re=EC=97=90=20=ED=86=A0=ED=81=B0=EC=9D=84=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EB=91=90=20=EB=B2=88=20=ED=98=B8=EC=B6=9C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=ED=95=98=EB=A9=B0,=20=EA=B5=AC=EA=B8=80=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=EA=B3=BC=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EB=B9=84?= =?UTF-8?q?=EC=8A=B7=ED=95=98=EA=B2=8C=20=EC=88=98=EC=A0=95=ED=95=98?= =?UTF-8?q?=EC=98=80=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/data/datastore/OAuthDataStore.kt | 17 +++++++--- .../data/datastore/OAuthDataStoreKeys.kt | 3 +- .../ui/auth/signup/SignUpViewModel.kt | 32 +++++++++---------- .../presentation/ui/login/LoginViewModel.kt | 9 +++--- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt b/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt index 9fbaf319..9e6c97e4 100644 --- a/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt +++ b/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStore.kt @@ -11,12 +11,20 @@ class OAuthDataStore @Inject constructor(@ApplicationContext context: Context) { private val Context.dataStore by preferencesDataStore(name = "oauth_pref") private val dataStore = context.dataStore - suspend fun saveIdToken(token: String) { - dataStore.edit { it[OAuthDataStoreKeys.GOOGLE_ID_TOKEN] = token } + suspend fun saveIdToken(platform: String, token: String) { + if (platform == OAuthProvider.GOOGLE.platform) { + dataStore.edit { it[OAuthDataStoreKeys.GOOGLE_ID_TOKEN] = token } + } else { + dataStore.edit { it[OAuthDataStoreKeys.KAKAO_ID_TOKEN] = token } + } } - suspend fun getIdToken(): String? { - return dataStore.data.first()[OAuthDataStoreKeys.GOOGLE_ID_TOKEN] + suspend fun getIdToken(platform: String): String? { + return if (platform == OAuthProvider.GOOGLE.platform) { + dataStore.data.first()[OAuthDataStoreKeys.GOOGLE_ID_TOKEN] + } else { + dataStore.data.first()[OAuthDataStoreKeys.KAKAO_ID_TOKEN] + } } suspend fun savePlatform(provider: OAuthProvider) { @@ -32,6 +40,7 @@ class OAuthDataStore @Inject constructor(@ApplicationContext context: Context) { suspend fun clear() { dataStore.edit { it.remove(OAuthDataStoreKeys.GOOGLE_ID_TOKEN) + it.remove(OAuthDataStoreKeys.KAKAO_ID_TOKEN) it.remove(OAuthDataStoreKeys.OAUTH_PLATFORM) } } diff --git a/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStoreKeys.kt b/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStoreKeys.kt index 9bdac71b..97af4495 100644 --- a/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStoreKeys.kt +++ b/app/src/main/java/com/sopt/clody/data/datastore/OAuthDataStoreKeys.kt @@ -3,6 +3,7 @@ package com.sopt.clody.data.datastore import androidx.datastore.preferences.core.stringPreferencesKey object OAuthDataStoreKeys { - val GOOGLE_ID_TOKEN = stringPreferencesKey("google_id_token") val OAUTH_PLATFORM = stringPreferencesKey("oauth_platform") + val GOOGLE_ID_TOKEN = stringPreferencesKey("google_id_token") + val KAKAO_ID_TOKEN = stringPreferencesKey("kakao_id_token") } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index b9ee5e6d..ffe4a5ae 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -156,7 +156,7 @@ class SignUpViewModel @AssistedInject constructor( val fcmToken = fcmTokenProvider.getToken().orEmpty() if (platform == OAuthProvider.GOOGLE) { - val idToken = oAuthDataStore.getIdToken() + val idToken = oAuthDataStore.getIdToken(platform = "google") if (idToken.isNullOrBlank()) { setState { copy(errorMessage = "Google ID Token이 없습니다.", isLoading = false) } return @@ -168,33 +168,31 @@ class SignUpViewModel @AssistedInject constructor( ) val result = authRepository.signUp("Bearer $idToken", request) - handleSignUpResult(result, isGoogle = true) + handleSignUpResult(result) } else { - loginSdk.login(context).fold( - onSuccess = { token -> - val request = SignUpRequestDto( - platform = OAuthProvider.KAKAO.platform, - name = state.nickname, - fcmToken = fcmToken, - ) - val result = authRepository.signUp("Bearer ${token.value}", request) - handleSignUpResult(result, isGoogle = false) - }, - onFailure = { - setState { copy(errorMessage = "로그인에 실패했어요~", isLoading = false) } - }, + val idToken = oAuthDataStore.getIdToken(platform = "kakao") + if (idToken.isNullOrBlank()) { + setState { copy(errorMessage = "Kakao ID Token이 없습니다.", isLoading = false) } + return + } + val request = SignUpRequestDto( + platform = OAuthProvider.KAKAO.platform, + name = state.nickname, + fcmToken = fcmToken, ) + + val result = authRepository.signUp("Bearer $idToken", request) + handleSignUpResult(result) } } private suspend fun handleSignUpResult( result: Result, - isGoogle: Boolean, ) { result.fold( onSuccess = { tokenRepository.setTokens(it.accessToken, it.refreshToken) - if (isGoogle) oAuthDataStore.clear() + oAuthDataStore.clear() _sideEffects.send(SignUpContract.SignUpSideEffect.NavigateToTimeReminder) }, onFailure = { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt index 498fa34b..c7540425 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -99,6 +99,8 @@ class LoginViewModel @AssistedInject constructor( setState { copy(isLoading = false) } val msg = error.message.orEmpty() if (msg.contains("404")) { + oauthDataStore.saveIdToken(platform = "kakao", token = kakaoToken) + oauthDataStore.savePlatform(OAuthProvider.KAKAO) _sideEffects.send(LoginContract.LoginSideEffect.NavigateToSignUp) } else { _sideEffects.send(LoginContract.LoginSideEffect.ShowError(msg)) @@ -109,10 +111,7 @@ class LoginViewModel @AssistedInject constructor( private suspend fun validateGoogleUser(idToken: String) { val fcmToken = fcmTokenProvider.getToken().orEmpty() - val request = GoogleSignUpRequestDto( - idToken = idToken, - fcmToken = fcmToken, - ) + val request = GoogleSignUpRequestDto(idToken = idToken, fcmToken = fcmToken) authRepository.signUpWithGoogle(request).fold( onSuccess = { @@ -125,7 +124,7 @@ class LoginViewModel @AssistedInject constructor( val msg = error.message.orEmpty() if (msg.contains("404")) { - oauthDataStore.saveIdToken(idToken) + oauthDataStore.saveIdToken(platform = "google", token = idToken) oauthDataStore.savePlatform(OAuthProvider.GOOGLE) _sideEffects.send(LoginContract.LoginSideEffect.NavigateToSignUp) } else { From de0d166a17fc3d6a9860fad373b5d005ab9bcbd3 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 4 Aug 2025 00:07:23 +0900 Subject: [PATCH 281/299] =?UTF-8?q?[ADD/#297]=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=EC=97=B0=EA=B2=B0=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?Observer=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/network/NetworkConnectivityModule.kt | 22 ++++++ .../network/NetworkConnectivityObserver.kt | 79 +++++++++++++++++++ .../sopt/clody/core/network/NetworkStatus.kt | 6 ++ 3 files changed, 107 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityModule.kt create mode 100644 app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityObserver.kt create mode 100644 app/src/main/java/com/sopt/clody/core/network/NetworkStatus.kt diff --git a/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityModule.kt b/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityModule.kt new file mode 100644 index 00000000..9ce6bca0 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityModule.kt @@ -0,0 +1,22 @@ +package com.sopt.clody.core.network + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkConnectivityModule { + + @Provides + @Singleton + fun provideNetworkConnectivityObserver( + @ApplicationContext context: Context, + ): NetworkConnectivityObserver { + return NetworkConnectivityObserver(context) + } +} diff --git a/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityObserver.kt b/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityObserver.kt new file mode 100644 index 00000000..626e4b43 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityObserver.kt @@ -0,0 +1,79 @@ +package com.sopt.clody.core.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import javax.inject.Inject + +/** + * 네트워크 연결 상태를 관찰하는 Observer. + * + * - `Available`: 인터넷에 연결되어 있음 + * - `Unavailable`: 인터넷 연결이 끊긴 상태 + * + */ +class NetworkConnectivityObserver @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + /** + * 네트워크 상태를 실시간으로 스트리밍하는 Flow. + * + * - 최초 구독 시 현재 상태를 먼저 전송 -> + * - 이후 네트워크 변경 이벤트를 수신하여 상태를 전송 + * - 중복 상태 전송은 [distinctUntilChanged]로 방지 하도록 함. + */ + val networkStatus: Flow = callbackFlow { + val callback = object : ConnectivityManager.NetworkCallback() { + + /** + * 네트워크가 변경되었을 때 호출됨. + * 유효한 인터넷 연결이 있는지 확인하여 상태를 전송. + */ + override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) { + val hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + trySend(if (hasInternet) NetworkStatus.Available else NetworkStatus.Unavailable) + } + + /** + * 네트워크 연결이 완전히 끊겼을 때 호출. + */ + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable) + } + } + + trySend(if (isCurrentlyAvailable()) NetworkStatus.Available else NetworkStatus.Unavailable) + + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + + connectivityManager.registerNetworkCallback(request, callback) + + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + } + }.distinctUntilChanged() + + /** + * 현재 활성 네트워크가 인터넷에 연결되어 있는지를 반환 + * + * @return 인터넷 연결 여부 + */ + private fun isCurrentlyAvailable(): Boolean { + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } +} diff --git a/app/src/main/java/com/sopt/clody/core/network/NetworkStatus.kt b/app/src/main/java/com/sopt/clody/core/network/NetworkStatus.kt new file mode 100644 index 00000000..9f28341a --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/network/NetworkStatus.kt @@ -0,0 +1,6 @@ +package com.sopt.clody.core.network + +sealed class NetworkStatus { + data object Available : NetworkStatus() + data object Unavailable : NetworkStatus() +} From 65122a76ce6bf5d3c29bdddae803d3e759a19d8a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 4 Aug 2025 00:08:55 +0900 Subject: [PATCH 282/299] =?UTF-8?q?[DEL#297]=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EC=9C=A0=ED=8B=B8=EB=A6=AC=ED=8B=B0=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/clody/data/remote/util/NetworkUtil.kt | 12 ------------ app/src/main/java/com/sopt/clody/di/NetworkModule.kt | 9 --------- 2 files changed, 21 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/data/remote/util/NetworkUtil.kt diff --git a/app/src/main/java/com/sopt/clody/data/remote/util/NetworkUtil.kt b/app/src/main/java/com/sopt/clody/data/remote/util/NetworkUtil.kt deleted file mode 100644 index 798737f1..00000000 --- a/app/src/main/java/com/sopt/clody/data/remote/util/NetworkUtil.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.sopt.clody.data.remote.util - -import android.net.ConnectivityManager -import android.net.NetworkCapabilities - -class NetworkUtil(private val connectivityManager: ConnectivityManager) { - fun isNetworkAvailable(): Boolean { - val network = connectivityManager.activeNetwork ?: return false - val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false - return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - } -} diff --git a/app/src/main/java/com/sopt/clody/di/NetworkModule.kt b/app/src/main/java/com/sopt/clody/di/NetworkModule.kt index da77ddda..3fc7aeb5 100644 --- a/app/src/main/java/com/sopt/clody/di/NetworkModule.kt +++ b/app/src/main/java/com/sopt/clody/di/NetworkModule.kt @@ -1,12 +1,10 @@ package com.sopt.clody.di import android.content.Context -import android.net.ConnectivityManager import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.sopt.clody.BuildConfig import com.sopt.clody.data.datastore.TokenDataStore import com.sopt.clody.data.remote.util.AuthInterceptor -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.data.remote.util.TimeZoneInterceptor import com.sopt.clody.domain.repository.TokenReissueRepository import dagger.Module @@ -79,11 +77,4 @@ object NetworkModule { .client(okHttpClient) .build() } - - @Provides - @Singleton - fun provideNetworkUtil(@ApplicationContext context: Context): NetworkUtil { - val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - return NetworkUtil(connectivityManager) - } } From e83a7a8b928fe207c087d96f0b00d33a8a22a101 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 4 Aug 2025 00:09:17 +0900 Subject: [PATCH 283/299] =?UTF-8?q?[REFACTOR/#297]=20Diary=20UseCase=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/domain/usecase/FetchDraftDiaryUseCase.kt | 8 +++----- .../clody/domain/usecase/SaveDraftDiaryUseCase.kt | 5 ++--- .../sopt/clody/domain/usecase/WriteDiaryUseCase.kt | 11 +++++++++++ 3 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt index 929c2e1f..382be324 100644 --- a/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt +++ b/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt @@ -1,13 +1,11 @@ package com.sopt.clody.domain.usecase -import com.sopt.clody.domain.model.DraftDiaryContents import com.sopt.clody.domain.repository.DiaryRepository import javax.inject.Inject class FetchDraftDiaryUseCase @Inject constructor( - private val repository: DiaryRepository, + private val diaryRepository: DiaryRepository, ) { - suspend operator fun invoke(year: Int, month: Int, day: Int): Result { - return repository.fetchDraftDiary(year, month, day) - } + suspend operator fun invoke(year: Int, month: Int, day: Int) = + diaryRepository.fetchDraftDiary(year, month, day) } diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt index a1b2ffa6..e37e19cc 100644 --- a/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt +++ b/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt @@ -6,7 +6,6 @@ import javax.inject.Inject class SaveDraftDiaryUseCase @Inject constructor( private val diaryRepository: DiaryRepository, ) { - suspend operator fun invoke(date: String, contents: List): Result { - return diaryRepository.saveDraftDiary(date, contents) - } + suspend operator fun invoke(date: String, contents: List) = + diaryRepository.saveDraftDiary(date, contents) } diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt new file mode 100644 index 00000000..fa00f549 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt @@ -0,0 +1,11 @@ +package com.sopt.clody.domain.usecase + +import com.sopt.clody.domain.repository.DiaryRepository +import javax.inject.Inject + +class WriteDiaryUseCase @Inject constructor( + private val diaryRepository: DiaryRepository, +) { + suspend operator fun invoke(date: String, content: List) = + diaryRepository.writeDiary(date, content) +} From 82cdd7fa9381807cd889ec0cda38356e3bee3757 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 4 Aug 2025 00:10:06 +0900 Subject: [PATCH 284/299] =?UTF-8?q?[ADD/#297]=20ErrorMessageProvider=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 - 에러 메시지를 제공하는 ErrorMessageProvider 인터페이스와 구현체 ErrorMessageProviderImpl을 추가했습니다. - ErrorMessageModule을 추가하여 ErrorMessageProvider를 주입할 수 있도록 설정했습니다. --- .../utils/network/ErrorMessageModule.kt | 22 ++++++++ .../utils/network/ErrorMessageProvider.kt | 16 ++++++ .../utils/network/ErrorMessageProviderImpl.kt | 52 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageModule.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProvider.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProviderImpl.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageModule.kt b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageModule.kt new file mode 100644 index 00000000..71eb58cc --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageModule.kt @@ -0,0 +1,22 @@ +package com.sopt.clody.presentation.utils.network + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ErrorMessageModule { + + @Provides + @Singleton + fun provideErrorMessageProvider( + @ApplicationContext context: Context, + ): ErrorMessageProvider { + return ErrorMessageProviderImpl(context) + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProvider.kt b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProvider.kt new file mode 100644 index 00000000..bac76132 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProvider.kt @@ -0,0 +1,16 @@ +package com.sopt.clody.presentation.utils.network + +import com.sopt.clody.data.remote.util.ApiError + +interface ErrorMessageProvider { + fun getTemporaryError(): String + fun getNetworkError(): String + fun getServerError(): String + fun getFetchTempDiaryFailedError(): String + fun getUnknownError(): String + fun getLoginFailedError(): String + fun getSignupFailedError(): String + fun getGoogleIdTokenMissingError(): String + fun getNetworkCheckError(): String + fun getApiError(apiError: ApiError): String +} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProviderImpl.kt b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProviderImpl.kt new file mode 100644 index 00000000..13b6f193 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProviderImpl.kt @@ -0,0 +1,52 @@ +package com.sopt.clody.presentation.utils.network + +import android.content.Context +import com.sopt.clody.R +import com.sopt.clody.data.remote.util.ApiError +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class ErrorMessageProviderImpl @Inject constructor( + @ApplicationContext private val context: Context, +) : ErrorMessageProvider { + + override fun getTemporaryError(): String { + return context.getString(R.string.error_temporary) + } + + override fun getNetworkError(): String { + return context.getString(R.string.error_network) + } + + override fun getServerError(): String { + return context.getString(R.string.error_server) + } + + override fun getFetchTempDiaryFailedError(): String { + return context.getString(R.string.error_fetch_temp_diary_failed) + } + + override fun getUnknownError(): String { + return context.getString(R.string.error_unknown) + } + + override fun getLoginFailedError(): String { + return context.getString(R.string.error_login_failed) + } + + override fun getSignupFailedError(): String { + return context.getString(R.string.error_signup_failed) + } + + override fun getGoogleIdTokenMissingError(): String { + return context.getString(R.string.error_google_id_token_missing) + } + + override fun getNetworkCheckError(): String { + return context.getString(R.string.error_network_check) + } + + override fun getApiError(apiError: ApiError): String { + return apiError.message + } +} From 43e3aee0a2b780a1080effdeaeed9fe297bbc91a Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 4 Aug 2025 00:10:22 +0900 Subject: [PATCH 285/299] =?UTF-8?q?[CHORE/#297]=20ErrorMessages=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/presentation/utils/network/ErrorMessages.kt | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt deleted file mode 100644 index 3db50f7e..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.sopt.clody.presentation.utils.network - -object ErrorMessages { - const val FAILURE_NETWORK_MESSAGE = "서비스 접속이 원활하지 않아요.\n네트워크 연결을 확인해주세요." - const val FAILURE_TEMPORARY_MESSAGE = "일시적인 오류가 발생했어요.\n잠시 후 다시 시도해주세요." - const val FAILURE_SERVER_MESSAGE = "서버 오류가 발생했어요.\n잠시 후 다시 시도해주세요." - const val FETCH_TEMP_DIARY_FAILED = "임시저장 불러오기에 실패했어요.\n잠시 후 다시 시도해주세요." - const val UNKNOWN_ERROR = "알수없는 에러" -} From d87d6cdbbfd526a3cc2ae19676bbed07eb241c45 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 4 Aug 2025 00:11:19 +0900 Subject: [PATCH 286/299] =?UTF-8?q?[FEAT/#314]=20=EB=94=A5=EB=A7=81?= =?UTF-8?q?=ED=81=AC=20=EC=B6=94=EC=A0=81=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=97=90=EC=96=B4=EB=B8=8C=EB=A6=BF=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20airbridge?= =?UTF-8?q?=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20dataExtractionRules,=20fullBackupContent?= =?UTF-8?q?=20=EB=B6=80=EB=B6=84=EC=97=90=EC=84=9C=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EC=84=A0=EC=96=B8=EC=9D=B4=20=EC=9D=B4=EB=A3=A8=EC=96=B4?= =?UTF-8?q?=EC=A0=B8=20replace=EB=A5=BC=20=ED=99=95=EC=9E=A5=ED=96=88?= =?UTF-8?q?=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 3 ++- app/src/main/java/com/sopt/clody/ClodyApplication.kt | 8 ++++++++ gradle/libs.versions.toml | 3 +++ settings.gradle.kts | 1 + 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fd21eb50..0812d8ce 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -157,4 +157,5 @@ dependencies { implementation(libs.androidx.credentials.play.services.auth) implementation(libs.google.auth) implementation(libs.androidx.datastore.preferences) + implementation(libs.airbridge) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 341bddc1..95012ebc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + @@ -19,7 +20,7 @@ android:supportsRtl="true" android:theme="@style/Theme.CLODY" android:usesCleartextTraffic="true" - tools:replace="icon, label" + tools:replace="android:icon,android:label,android:dataExtractionRules,android:fullBackupContent" tools:targetApi="31"> Date: Mon, 4 Aug 2025 00:11:37 +0900 Subject: [PATCH 287/299] =?UTF-8?q?[ADD/#297]=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=AC=B8=EC=9E=90=EC=97=B4=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 --- app/src/main/res/values-ko/strings.xml | 11 +++++++++++ app/src/main/res/values/strings.xml | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bf0e2d55..798236a0 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -195,4 +195,15 @@ 일기 삭제 중 오류가 발생했습니다. Chrome이 설치되어 있어야 로그인이 가능합니다. 루팅된 기기에서는 로그인할 수 없습니다. + + + 서비스 접속이 원활하지 않아요.\n네트워크 연결을 확인해주세요. + 일시적인 오류가 발생했어요.\n잠시 후 다시 시도해주세요. + 서버 오류가 발생했어요.\n잠시 후 다시 시도해주세요. + 임시저장 불러오기에 실패했어요.\n잠시 후 다시 시도해주세요. + 알수없는 에러 + 로그인에 실패했어요. + 회원가입에 실패했어요. + Google ID Token이 없습니다. + 네트워크 연결을 확인해주세요. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e026628a..1d31330a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -196,4 +196,15 @@ Login is not available on rooted devices for security reasons. Failed to load data. An unexpected error has occurred. + + + Network connection is unstable.\nPlease check your network connection. + A temporary error has occurred.\nPlease try again later. + A server error has occurred.\nPlease try again later. + Failed to load draft.\nPlease try again later. + Unknown error + Login failed. + Sign up failed. + Google ID Token is missing. + Please check your network connection. From 2adabf1dbdcd8dd2a301b9364c9b8d953abca50c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 4 Aug 2025 00:13:58 +0900 Subject: [PATCH 288/299] =?UTF-8?q?[REFACTOR/#297]=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=EC=9C=A0=ED=8B=B8=EB=A6=AC=ED=8B=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ViewModel에서 NetworkUtil 대신 NetworkConnectivityObserver 사용 - 에러 메시지를 ErrorMessages 객체 대신 ErrorMessageProvider를 통해 제공하도록 수정 - 각 ViewModel의 네트워크 요청 및 에러 처리 로직에 변경된 방식 적용 --- .../ui/auth/signup/SignUpViewModel.kt | 18 ++++--- .../timereminder/TimeReminderViewModel.kt | 19 ++++---- .../ui/diarylist/screen/DiaryListViewModel.kt | 29 +++++------ .../ui/home/screen/HomeViewModel.kt | 44 +++++++++-------- .../presentation/ui/login/LoginViewModel.kt | 6 ++- .../ui/replydiary/ReplyDiaryViewModel.kt | 36 ++++++++------ .../screen/ReplyLoadingViewModel.kt | 48 +++++++++---------- .../screen/NotificationSettingViewModel.kt | 37 +++++++------- .../screen/AccountManagementViewModel.kt | 37 +++++++------- .../writediary/screen/WriteDiaryViewModel.kt | 24 +++++----- 10 files changed, 159 insertions(+), 139 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index 3ecb7f01..d1ad2f10 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -8,20 +8,23 @@ import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.airbnb.mvrx.withState import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.core.login.LoginSdk +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.datastore.OAuthDataStore import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.dto.response.SignUpResponseDto -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls import com.sopt.clody.presentation.utils.language.LanguageProvider +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow @@ -34,8 +37,9 @@ class SignUpViewModel @AssistedInject constructor( private val tokenRepository: TokenRepository, private val fcmTokenProvider: FcmTokenProvider, private val oAuthDataStore: OAuthDataStore, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, private val languageProvider: LanguageProvider, + private val errorMessageProvider: ErrorMessageProvider, ) : MavericksViewModel(initialState) { private val _intents = Channel(BUFFERED) @@ -145,8 +149,8 @@ class SignUpViewModel @AssistedInject constructor( private suspend fun signUp(context: Context) { val state = withState(this@SignUpViewModel) { it } - if (!networkUtil.isNetworkAvailable()) { - setState { copy(errorMessage = "네트워크 연결을 확인해주세요.") } + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + setState { copy(errorMessage = errorMessageProvider.getNetworkCheckError()) } return } @@ -158,7 +162,7 @@ class SignUpViewModel @AssistedInject constructor( if (platform == OAuthProvider.GOOGLE) { val idToken = oAuthDataStore.getIdToken() if (idToken.isNullOrBlank()) { - setState { copy(errorMessage = "Google ID Token이 없습니다.", isLoading = false) } + setState { copy(errorMessage = errorMessageProvider.getGoogleIdTokenMissingError(), isLoading = false) } return } val request = SignUpRequestDto( @@ -181,7 +185,7 @@ class SignUpViewModel @AssistedInject constructor( handleSignUpResult(result, isGoogle = false) }, onFailure = { - setState { copy(errorMessage = "로그인에 실패했어요~", isLoading = false) } + setState { copy(errorMessage = errorMessageProvider.getLoginFailedError(), isLoading = false) } }, ) } @@ -198,7 +202,7 @@ class SignUpViewModel @AssistedInject constructor( _sideEffects.send(SignUpContract.SignUpSideEffect.NavigateToTimeReminder) }, onFailure = { - setState { copy(errorMessage = "회원가입에 실패했어요~") } + setState { copy(errorMessage = errorMessageProvider.getSignupFailedError()) } }, ) setState { copy(isLoading = false) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt index 6e34cc55..07fba0d3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt @@ -6,24 +6,25 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.presentation.utils.extension.TimePeriod import com.sopt.clody.presentation.utils.extension.convertUTZtoKST -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class TimeReminderViewModel @Inject constructor( private val notificationRepository: NotificationRepository, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, + private val errorMessageProvider: ErrorMessageProvider, ) : ViewModel() { private val _timeReminderState = MutableStateFlow(TimeReminderState.Idle) @@ -34,8 +35,8 @@ class TimeReminderViewModel @Inject constructor( fun sendNotification(context: Context, isPermissionGranted: Boolean) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _timeReminderState.value = TimeReminderState.Failure(FAILURE_NETWORK_MESSAGE) + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _timeReminderState.value = TimeReminderState.Failure(errorMessageProvider.getNetworkError()) return@launch } @@ -60,9 +61,9 @@ class TimeReminderViewModel @Inject constructor( }, onFailure = { error -> val errorMessage = if (error.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - error.localizedMessage ?: UNKNOWN_ERROR + error.localizedMessage ?: errorMessageProvider.getUnknownError() } _timeReminderState.value = TimeReminderState.Failure(errorMessage) }, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt index f3ea9ae2..dff813a9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt @@ -2,14 +2,14 @@ package com.sopt.clody.presentation.ui.diarylist.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.domain.repository.DiaryRepository -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.time.LocalDate import java.time.format.TextStyle @@ -19,7 +19,8 @@ import javax.inject.Inject @HiltViewModel class DiaryListViewModel @Inject constructor( private val diaryRepository: DiaryRepository, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, + private val errorMessageProvider: ErrorMessageProvider, ) : ViewModel() { private val _diaryListState = MutableStateFlow(DiaryListState.Idle) @@ -44,8 +45,8 @@ class DiaryListViewModel @Inject constructor( if (retryCount >= maxRetryCount) return _diaryListState.value = DiaryListState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _diaryListState.value = DiaryListState.Failure(FAILURE_NETWORK_MESSAGE) + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _diaryListState.value = DiaryListState.Failure(errorMessageProvider.getNetworkError()) return@launch } val result = diaryRepository.getMonthlyDiary(year, month) @@ -57,12 +58,12 @@ class DiaryListViewModel @Inject constructor( onFailure = { retryCount++ if (retryCount >= maxRetryCount) { - DiaryListState.Failure(FAILURE_TEMPORARY_MESSAGE) + DiaryListState.Failure(errorMessageProvider.getTemporaryError()) } else { val errorMessage = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - it.localizedMessage ?: UNKNOWN_ERROR + errorMessageProvider.getUnknownError() } DiaryListState.Failure(errorMessage) } @@ -85,8 +86,8 @@ class DiaryListViewModel @Inject constructor( fun deleteDailyDiary(year: Int, month: Int, day: Int) { _diaryDeleteState.value = DiaryDeleteState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureDialogMessage.value = errorMessageProvider.getNetworkError() DiaryDeleteState.Failure(_failureDialogMessage.value) _showDiaryDeleteFailureDialog.value = true return@launch @@ -98,9 +99,9 @@ class DiaryListViewModel @Inject constructor( }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - it.localizedMessage ?: UNKNOWN_ERROR + it.localizedMessage ?: errorMessageProvider.getUnknownError() } _showDiaryDeleteFailureDialog.value = true DiaryDeleteState.Failure(_failureDialogMessage.value) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 3518c1ba..fc5121fb 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -1,14 +1,14 @@ package com.sopt.clody.presentation.ui.home.screen -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.core.fcm.FcmTokenProvider +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository @@ -16,10 +16,11 @@ import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.ReviewRepository import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState -import com.sopt.clody.presentation.utils.network.ErrorMessages +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.time.LocalDate import java.time.ZoneId @@ -29,10 +30,11 @@ import javax.inject.Inject class HomeViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val notificationRepository: NotificationRepository, - private val networkUtil: NetworkUtil, private val draftRepository: DraftRepository, private val fcmTokenProvider: FcmTokenProvider, private val reviewRepository: ReviewRepository, + private val errorMessageProvider: ErrorMessageProvider, + private val networkConnectivityObserver: NetworkConnectivityObserver, ) : ViewModel() { private val _calendarState = MutableStateFlow>(CalendarState.Idle) @@ -111,14 +113,14 @@ class HomeViewModel @Inject constructor( } } - fun setErrorState(isError: Boolean, message: String = ErrorMessages.FAILURE_TEMPORARY_MESSAGE) { + fun setErrorState(isError: Boolean, message: String = errorMessageProvider.getTemporaryError()) { _errorState.value = isError to message } fun loadCalendarData(year: Int, month: Int) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - setErrorState(true, ErrorMessages.FAILURE_NETWORK_MESSAGE) + if (!isNetworkAvailable()) { + setErrorState(true, errorMessageProvider.getNetworkError()) return@launch } @@ -130,8 +132,8 @@ class HomeViewModel @Inject constructor( CalendarState.Success(it) }, onFailure = { exception -> - setErrorState(true, exception.message ?: ErrorMessages.UNKNOWN_ERROR) - CalendarState.Error(exception.message ?: ErrorMessages.UNKNOWN_ERROR) + setErrorState(true, errorMessageProvider.getTemporaryError()) + CalendarState.Error(errorMessageProvider.getTemporaryError()) }, ) } @@ -139,8 +141,8 @@ class HomeViewModel @Inject constructor( fun loadDailyDiariesData(year: Int, month: Int, date: Int) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - setErrorState(true, ErrorMessages.FAILURE_NETWORK_MESSAGE) + if (!isNetworkAvailable()) { + setErrorState(true, errorMessageProvider.getNetworkError()) return@launch } @@ -156,8 +158,8 @@ class HomeViewModel @Inject constructor( DailyDiariesState.Success(dailyResponse) }, onFailure = { exception -> - setErrorState(true, exception.message ?: ErrorMessages.UNKNOWN_ERROR) - DailyDiariesState.Error(exception.message ?: ErrorMessages.UNKNOWN_ERROR) + setErrorState(true, errorMessageProvider.getTemporaryError()) + DailyDiariesState.Error(errorMessageProvider.getTemporaryError()) }, ) } @@ -178,7 +180,7 @@ class HomeViewModel @Inject constructor( DeleteDiaryState.Success }, onFailure = { - DeleteDiaryState.Failure(it.message ?: "Unknown error") + DeleteDiaryState.Failure(it.message ?: errorMessageProvider.getTemporaryError()) }, ) } @@ -254,10 +256,10 @@ class HomeViewModel @Inject constructor( return selected == today || selected == today.minusDays(1) } - fun enableDraftAlarm(context: Context) { + fun enableDraftAlarm() { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - setErrorState(true, ErrorMessages.FAILURE_NETWORK_MESSAGE) + if (!isNetworkAvailable()) { + setErrorState(true, errorMessageProvider.getNetworkError()) return@launch } @@ -268,9 +270,13 @@ class HomeViewModel @Inject constructor( } } + private suspend fun isNetworkAvailable(): Boolean { + return networkConnectivityObserver.networkStatus.first() == NetworkStatus.Available + } + private suspend fun getNotificationInfo(): NotificationInfoResponseDto? { return notificationRepository.getNotificationInfo().getOrElse { - _draftAlarmChangeState.value = NotificationChangeState.Failure("알림 정보를 가져오는데 실패했습니다.") + _draftAlarmChangeState.value = NotificationChangeState.Failure(errorMessageProvider.getTemporaryError()) null } } @@ -293,7 +299,7 @@ class HomeViewModel @Inject constructor( _draftAlarmChangeState.value = NotificationChangeState.Success(it) }, onFailure = { - _draftAlarmChangeState.value = NotificationChangeState.Failure("이어쓰기 알림 설정에 실패했습니다.") + _draftAlarmChangeState.value = NotificationChangeState.Failure(errorMessageProvider.getTemporaryError()) }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt index e5d53217..09af1cb5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -14,6 +14,7 @@ import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository import com.sopt.clody.presentation.ui.login.LoginContract.LoginIntent import com.sopt.clody.presentation.utils.language.LanguageProvider +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -32,6 +33,7 @@ class LoginViewModel @AssistedInject constructor( private val fcmTokenProvider: FcmTokenProvider, private val oauthDataStore: OAuthDataStore, private val languageProvider: LanguageProvider, + private val errorMessageProvider: ErrorMessageProvider, ) : MavericksViewModel(initialState) { private val _intents = Channel(BUFFERED) @@ -68,7 +70,7 @@ class LoginViewModel @AssistedInject constructor( }, onFailure = { error -> setState { copy(isLoading = false) } - _sideEffects.send(LoginContract.LoginSideEffect.ShowError("로그인에 실패했습니다")) + _sideEffects.send(LoginContract.LoginSideEffect.ShowError(errorMessageProvider.getLoginFailedError())) }, ) } @@ -76,7 +78,7 @@ class LoginViewModel @AssistedInject constructor( OAuthProvider.GOOGLE -> { val idToken = intent.idToken if (idToken.isNullOrBlank()) { - _sideEffects.send(LoginContract.LoginSideEffect.ShowError("로그인에 실패했습니다.")) + _sideEffects.send(LoginContract.LoginSideEffect.ShowError(errorMessageProvider.getLoginFailedError())) return } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryViewModel.kt index 9dc9db8c..4a93338f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryViewModel.kt @@ -2,17 +2,18 @@ package com.sopt.clody.presentation.ui.replydiary import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto -import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.data.remote.util.ApiError import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.presentation.utils.extension.throttleFirst -import com.sopt.clody.presentation.utils.network.ErrorMessages -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -22,7 +23,8 @@ import javax.inject.Inject @HiltViewModel class ReplyDiaryViewModel @Inject constructor( private val diaryRepository: DiaryRepository, - private val networkUtil: NetworkUtil, + private val errorMessageProvider: ErrorMessageProvider, + private val networkConnectivityObserver: NetworkConnectivityObserver, ) : ViewModel() { private val _replyDiaryState = MutableStateFlow(ReplyDiaryState.Idle) @@ -32,7 +34,7 @@ class ReplyDiaryViewModel @Inject constructor( private var lastMonth: Int = 0 private var lastDate: Int = 0 - private val _retryFlow = MutableSharedFlow() // 연속 클릭을 제어하기 위해 선언. + private val _retryFlow = MutableSharedFlow() init { setupRetryFlow() @@ -40,11 +42,11 @@ class ReplyDiaryViewModel @Inject constructor( private fun setupRetryFlow() { _retryFlow - .throttleFirst(2000L) // 2초 동안 첫 번째 이벤트만 발행. - .onEach { // Flow에서 발생한 이벤트를 받아서 getReplyDiaryInternal 호출. + .throttleFirst(2000L) + .onEach { getReplyDiaryInternal(lastYear, lastMonth, lastDate) } - .launchIn(viewModelScope) // Flow를 viewModelScope에서 실행하고 구독을 유지, 즉 viewmodel이 살아있는 동안 flow가 실행됨 + .launchIn(viewModelScope) } fun getReplyDiary(year: Int, month: Int, date: Int) { @@ -56,8 +58,9 @@ class ReplyDiaryViewModel @Inject constructor( private fun getReplyDiaryInternal(year: Int, month: Int, date: Int) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - updateState(ReplyDiaryState.Failure(FAILURE_NETWORK_MESSAGE)) + val isConnected = networkConnectivityObserver.networkStatus.first() == NetworkStatus.Available + if (!isConnected) { + updateState(ReplyDiaryState.Failure(errorMessageProvider.getNetworkError())) return@launch } @@ -73,7 +76,7 @@ class ReplyDiaryViewModel @Inject constructor( onSuccess = { data -> updateState( ReplyDiaryState.Success( - content = data.content ?: "", + content = data.content.orEmpty(), nickname = data.nickname, month = data.month, date = data.date, @@ -81,9 +84,12 @@ class ReplyDiaryViewModel @Inject constructor( ) }, onFailure = { throwable -> - updateState(ReplyDiaryState.Failure(ErrorMessages.FAILURE_TEMPORARY_MESSAGE)) - val errorMessage = throwable.localizedMessage ?: UNKNOWN_ERROR - Timber.tag("ReplyDiaryViewModel").e("API 요청 실패: %s", errorMessage) + val message = when (throwable) { + is ApiError -> errorMessageProvider.getApiError(throwable) + else -> errorMessageProvider.getTemporaryError() + } + updateState(ReplyDiaryState.Failure(message)) + Timber.tag("ReplyDiaryViewModel").e(throwable, "API 요청 실패") }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt index 3683b4a4..b66d127e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt @@ -4,18 +4,19 @@ import android.app.Activity import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.core.ad.RewardAdShower +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto -import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.data.remote.util.ApiError import com.sopt.clody.domain.repository.AdRepository import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.presentation.utils.extension.throttleFirst -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -27,8 +28,9 @@ import javax.inject.Inject class ReplyLoadingViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val adRepository: AdRepository, - private val networkUtil: NetworkUtil, private val rewardAdShower: RewardAdShower, + private val errorMessageProvider: ErrorMessageProvider, + private val networkConnectivityObserver: NetworkConnectivityObserver, ) : ViewModel() { private val _replyLoadingState = MutableStateFlow(ReplyLoadingState.Idle) @@ -65,9 +67,7 @@ class ReplyLoadingViewModel @Inject constructor( private fun setupRetryFlow() { _retryFlow .throttleFirst(2000L) - .onEach { - getDiaryTimeInternal(lastYear, lastMonth, lastDate) - } + .onEach { getDiaryTimeInternal(lastYear, lastMonth, lastDate) } .launchIn(viewModelScope) } @@ -82,8 +82,9 @@ class ReplyLoadingViewModel @Inject constructor( _replyLoadingState.value = ReplyLoadingState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _replyLoadingState.value = ReplyLoadingState.Failure(FAILURE_NETWORK_MESSAGE) + val isConnected = networkConnectivityObserver.networkStatus.first() == NetworkStatus.Available + if (!isConnected) { + _replyLoadingState.value = ReplyLoadingState.Failure(errorMessageProvider.getNetworkError()) return@launch } @@ -95,15 +96,9 @@ class ReplyLoadingViewModel @Inject constructor( private fun handleResult(result: Result) { result.fold( onSuccess = { data -> - val diaryWrittenDay = data.date.split("-") - var targetDateTime = LocalDateTime.of( - diaryWrittenDay[0].toInt(), - diaryWrittenDay[1].toInt(), - diaryWrittenDay[2].toInt(), - data.HH, - data.mm, - data.ss, - ).plusMinutes(if (data.isFirst) INITIAL_REMINDER_MINUTES else REGULAR_REMINDER_HOURS * 60) + val (y, m, d) = data.date.split("-").map { it.toInt() } + var targetDateTime = LocalDateTime.of(y, m, d, data.HH, data.mm, data.ss) + .plusMinutes(if (data.isFirst) INITIAL_REMINDER_MINUTES else REGULAR_REMINDER_HOURS * 60) if (_isAdCompleted.value || data.isFromAd) { targetDateTime = LocalDateTime.now() @@ -114,9 +109,13 @@ class ReplyLoadingViewModel @Inject constructor( _isWaitingForPatchResponse.value = false }, onFailure = { throwable -> - _replyLoadingState.value = ReplyLoadingState.Failure(FAILURE_TEMPORARY_MESSAGE) - val errorMessage = throwable.localizedMessage ?: UNKNOWN_ERROR - Timber.tag("ReplyLoadingViewModel").e("API 요청 실패: %s", errorMessage) + val message = if (throwable is ApiError) { + errorMessageProvider.getApiError(throwable) + } else { + errorMessageProvider.getTemporaryError() + } + _replyLoadingState.value = ReplyLoadingState.Failure(message) + Timber.tag("ReplyLoadingViewModel").e(throwable, "DiaryTime API 요청 실패") }, ) } @@ -138,7 +137,7 @@ class ReplyLoadingViewModel @Inject constructor( val startAdResult = adRepository.startAd(lastYear, lastMonth, lastDate) if (startAdResult.isFailure) { _isAdLoading.value = false - _adErrorMessage.value = "잠시 후 다시 시도해주세요!" + _adErrorMessage.value = errorMessageProvider.getTemporaryError() return@launch } @@ -152,7 +151,7 @@ class ReplyLoadingViewModel @Inject constructor( _isAdPreloaded = true showRewardedAd(activity) } else { - _adErrorMessage.value = "잠시 후 다시 시도해주세요!" + _adErrorMessage.value = errorMessageProvider.getTemporaryError() } } } @@ -164,7 +163,6 @@ class ReplyLoadingViewModel @Inject constructor( onAdRewarded = { viewModelScope.launch { _isWaitingForPatchResponse.value = true - adRepository.endAd(lastYear, lastMonth, lastDate).onSuccess { _replyLoadingState.value = ReplyLoadingState.Success(LocalDateTime.now()) _isWaitingForPatchResponse.value = false diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt index c29f3268..dfdc1d8b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt @@ -3,25 +3,26 @@ package com.sopt.clody.presentation.ui.setting.notificationsetting.screen import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.Notification import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.presentation.utils.extension.TimePeriod import com.sopt.clody.presentation.utils.extension.convertUTZtoKST -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class NotificationSettingViewModel @Inject constructor( private val notificationRepository: NotificationRepository, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, + private val errorMessageProvider: ErrorMessageProvider, ) : ViewModel() { private val _diaryAlarm = MutableStateFlow(false) @@ -62,8 +63,8 @@ class NotificationSettingViewModel @Inject constructor( if (retryCount >= maxRetryCount) return _notificationInfoState.value = NotificationInfoState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _notificationInfoState.value = NotificationInfoState.Failure(FAILURE_NETWORK_MESSAGE) + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _notificationInfoState.value = NotificationInfoState.Failure(errorMessageProvider.getNetworkError()) return@launch } val result = notificationRepository.getNotificationInfo() @@ -79,12 +80,12 @@ class NotificationSettingViewModel @Inject constructor( onFailure = { retryCount++ if (retryCount >= maxRetryCount) { - NotificationInfoState.Failure(FAILURE_TEMPORARY_MESSAGE) + NotificationInfoState.Failure(errorMessageProvider.getTemporaryError()) } else { if (it.message?.contains("200") == false) { - NotificationInfoState.Failure(FAILURE_TEMPORARY_MESSAGE) + NotificationInfoState.Failure(errorMessageProvider.getTemporaryError()) } else { - NotificationInfoState.Failure(UNKNOWN_ERROR) + NotificationInfoState.Failure(errorMessageProvider.getUnknownError()) } } }, @@ -95,8 +96,8 @@ class NotificationSettingViewModel @Inject constructor( fun changeAlarm(context: Context, notificationType: Notification) { _notificationChangeState.value = NotificationChangeState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureDialogMessage.value = errorMessageProvider.getNetworkError() _showFailureDialog.value = true return@launch } @@ -131,9 +132,9 @@ class NotificationSettingViewModel @Inject constructor( }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - UNKNOWN_ERROR + errorMessageProvider.getUnknownError() } _showFailureDialog.value = true _notificationChangeState.value = NotificationChangeState.Failure(_failureDialogMessage.value) @@ -145,8 +146,8 @@ class NotificationSettingViewModel @Inject constructor( fun changeNotificationTime(context: Context, timePeriod: TimePeriod, hour: String, minute: String) { _notificationTimeChangeState.value = NotificationTimeChangeState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureDialogMessage.value = errorMessageProvider.getNetworkError() _showFailureDialog.value = true return@launch } @@ -169,9 +170,9 @@ class NotificationSettingViewModel @Inject constructor( }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - UNKNOWN_ERROR + errorMessageProvider.getUnknownError() } _showFailureDialog.value = true _notificationTimeChangeState.value = NotificationTimeChangeState.Failure(_failureDialogMessage.value) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt index 976d9893..2c442ce6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt @@ -3,19 +3,19 @@ package com.sopt.clody.presentation.ui.setting.screen import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.datastore.TokenDataStore import com.sopt.clody.data.remote.dto.request.ModifyNicknameRequestDto -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AccountManagementRepository import com.sopt.clody.presentation.ui.auth.signup.NicknameMessage import com.sopt.clody.presentation.utils.language.LanguageProvider -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -23,9 +23,10 @@ import javax.inject.Inject class AccountManagementViewModel @Inject constructor( private val accountManagementRepository: AccountManagementRepository, private val tokenDataStore: TokenDataStore, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, @ApplicationContext private val context: Context, private val languageProvider: LanguageProvider, + private val errorMessageProvider: ErrorMessageProvider, ) : ViewModel() { private val _userInfoState = MutableStateFlow(UserInfoState.Idle) val userInfoState: StateFlow = _userInfoState @@ -61,8 +62,8 @@ class AccountManagementViewModel @Inject constructor( if (retryCount >= maxRetryCount) return _userInfoState.value = UserInfoState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _userInfoState.value = UserInfoState.Failure(FAILURE_NETWORK_MESSAGE) + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _userInfoState.value = UserInfoState.Failure(errorMessageProvider.getNetworkError()) return@launch } val result = accountManagementRepository.getUserInfo() @@ -75,12 +76,12 @@ class AccountManagementViewModel @Inject constructor( onFailure = { retryCount++ if (retryCount >= maxRetryCount) { - UserInfoState.Failure(FAILURE_TEMPORARY_MESSAGE) + UserInfoState.Failure(errorMessageProvider.getTemporaryError()) } else { val errorMessage = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - UNKNOWN_ERROR + errorMessageProvider.getUnknownError() } UserInfoState.Failure(errorMessage) } @@ -91,8 +92,8 @@ class AccountManagementViewModel @Inject constructor( fun changeNickname(modifyNicknameRequestDto: ModifyNicknameRequestDto) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureDialogMessage.value = errorMessageProvider.getNetworkError() _showFailureDialog.value = true } _userNicknameState.value = UserNicknameState.Loading @@ -101,9 +102,9 @@ class AccountManagementViewModel @Inject constructor( onSuccess = { UserNicknameState.Success(it) }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - UNKNOWN_ERROR + errorMessageProvider.getUnknownError() } _showFailureDialog.value = true UserNicknameState.Failure(_failureDialogMessage.value) @@ -142,8 +143,8 @@ class AccountManagementViewModel @Inject constructor( fun revokeAccount() { _revokeAccountState.value = RevokeAccountState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureDialogMessage.value = errorMessageProvider.getNetworkError() _showFailureDialog.value = true } val result = accountManagementRepository.revokeAccount() @@ -154,9 +155,9 @@ class AccountManagementViewModel @Inject constructor( }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - it.localizedMessage ?: UNKNOWN_ERROR + it.localizedMessage ?: errorMessageProvider.getUnknownError() } _showFailureDialog.value = true RevokeAccountState.Failure(_failureDialogMessage.value) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index 61c133cc..6cc4ca22 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -7,19 +7,18 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.usecase.FetchDraftDiaryUseCase import com.sopt.clody.domain.usecase.SaveDraftDiaryUseCase import com.sopt.clody.presentation.utils.language.LanguageProvider -import com.sopt.clody.presentation.utils.network.ErrorMessages -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.time.LocalDate import javax.inject.Inject @@ -29,9 +28,10 @@ class WriteDiaryViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val fetchDraftDiaryUseCase: FetchDraftDiaryUseCase, private val saveDraftDiaryUseCase: SaveDraftDiaryUseCase, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, private val draftRepository: DraftRepository, private val languageProvider: LanguageProvider, + private val errorMessageProvider: ErrorMessageProvider, ) : ViewModel() { private val _writeDiaryState = MutableStateFlow(WriteDiaryState.Idle) @@ -74,8 +74,8 @@ class WriteDiaryViewModel @Inject constructor( fun writeDiary(year: Int, month: Int, day: Int, contents: List) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureMessage.value = errorMessageProvider.getNetworkError() _showFailureDialog.value = true return@launch } @@ -96,9 +96,9 @@ class WriteDiaryViewModel @Inject constructor( }, onFailure = { _failureMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - it.localizedMessage ?: UNKNOWN_ERROR + it.localizedMessage ?: errorMessageProvider.getUnknownError() } _showFailureDialog.value = true WriteDiaryState.Failure(_failureMessage.value) @@ -217,7 +217,7 @@ class WriteDiaryViewModel @Inject constructor( checkEmptyFieldsMessage() }.onFailure { ensureDefaultEntry() - _failureMessage.value = ErrorMessages.FETCH_TEMP_DIARY_FAILED + _failureMessage.value = errorMessageProvider.getFetchTempDiaryFailedError() _showFailureDialog.value = true } } @@ -231,7 +231,7 @@ class WriteDiaryViewModel @Inject constructor( _failureMessage.value = "" _showFailureDialog.value = false }.onFailure { e -> - _failureMessage.value = e.localizedMessage ?: UNKNOWN_ERROR + _failureMessage.value = e.localizedMessage ?: errorMessageProvider.getUnknownError() _showFailureDialog.value = true } } From 91b35ac445cb1312b0ce7d194e1e814653a60f23 Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 4 Aug 2025 00:14:23 +0900 Subject: [PATCH 289/299] =?UTF-8?q?[REFACTOR/#297]=20enableDraftAlarm=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9D=B8=EC=9E=90=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 71956140..a5f9d9a0 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -197,7 +197,7 @@ fun HomeRoute( ClodyButton( text = stringResource(R.string.bottom_sheet_home_initial_draft_accept), onClick = { - homeViewModel.enableDraftAlarm(context) + homeViewModel.enableDraftAlarm() homeViewModel.updateFirstDraftUse(false) }, enabled = true, From 89fe3c8a9bcc156d62833c4fbf8abbe272e1a16c Mon Sep 17 00:00:00 2001 From: MoonsuKang Date: Mon, 4 Aug 2025 00:37:20 +0900 Subject: [PATCH 290/299] =?UTF-8?q?[REFACTOR/#297]=20=EC=9D=BC=EB=B6=80=20?= =?UTF-8?q?feature=EC=9D=84=20=EC=8B=A4=ED=97=98=EC=9A=A9=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20SafeApiCall=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datasource/DiaryRemoteDataSource.kt | 19 ++-- .../DiaryRemoteDataSourceImpl.kt | 48 ++++----- .../sopt/clody/data/remote/util/ApiError.kt | 5 + .../clody/data/remote/util/SafeApiCall.kt | 25 +++++ .../repositoryimpl/DiaryRepositoryImpl.kt | 102 ++++-------------- .../domain/repository/DiaryRepository.kt | 1 + .../datasource/FakeDiaryRemoteDataSource.kt | 27 ++--- 7 files changed, 96 insertions(+), 131 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/data/remote/util/ApiError.kt create mode 100644 app/src/main/java/com/sopt/clody/data/remote/util/SafeApiCall.kt diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt index 26c0d844..1b03559d 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt @@ -1,6 +1,5 @@ package com.sopt.clody.data.remote.datasource -import com.sopt.clody.data.remote.dto.base.ApiResponse import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto @@ -11,13 +10,13 @@ import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto interface DiaryRemoteDataSource { - suspend fun writeDiary(date: String, content: List): ApiResponse - suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): ApiResponse - suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): ApiResponse - suspend fun getDiaryTime(year: Int, month: Int, date: Int): ApiResponse - suspend fun getMonthlyCalendarData(year: Int, month: Int): ApiResponse - suspend fun getMonthlyDiary(year: Int, month: Int): ApiResponse - suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse - suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): ApiResponse - suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse + suspend fun writeDiary(date: String, content: List): Result + suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): Result + suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): Result + suspend fun getDiaryTime(year: Int, month: Int, date: Int): Result + suspend fun getMonthlyCalendarData(year: Int, month: Int): Result + suspend fun getMonthlyDiary(year: Int, month: Int): Result + suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result + suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result + suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): Result } diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt index f5e35d2a..9eaae300 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt @@ -2,45 +2,41 @@ package com.sopt.clody.data.remote.datasourceimpl import com.sopt.clody.data.remote.api.DiaryService import com.sopt.clody.data.remote.datasource.DiaryRemoteDataSource -import com.sopt.clody.data.remote.dto.base.ApiResponse import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.request.WriteDiaryRequestDto -import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto -import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto -import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto -import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto +import com.sopt.clody.data.remote.util.safeApiCall +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import javax.inject.Inject class DiaryRemoteDataSourceImpl @Inject constructor( private val diaryService: DiaryService, + private val errorMessageProvider: ErrorMessageProvider, ) : DiaryRemoteDataSource { - override suspend fun writeDiary(date: String, content: List): ApiResponse = - diaryService.writeDiary(WriteDiaryRequestDto(date, content)) - override suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): ApiResponse = - diaryService.deleteDailyDiary(year = year, month = month, date = date) + override suspend fun writeDiary(date: String, content: List) = + safeApiCall(errorMessageProvider) { diaryService.writeDiary(WriteDiaryRequestDto(date, content)) } - override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): ApiResponse = - diaryService.getDailyDiariesData(year = year, month = month, date = date) + override suspend fun deleteDailyDiary(year: Int, month: Int, date: Int) = + safeApiCall(errorMessageProvider) { diaryService.deleteDailyDiary(year, month, date) } - override suspend fun getDiaryTime(year: Int, month: Int, date: Int): ApiResponse = - diaryService.getDiaryTime(year = year, month = month, date = date) + override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int) = + safeApiCall(errorMessageProvider) { diaryService.getDailyDiariesData(year, month, date) } - override suspend fun getMonthlyCalendarData(year: Int, month: Int): ApiResponse = - diaryService.getMonthlyCalendarData(year = year, month = month) + override suspend fun getDiaryTime(year: Int, month: Int, date: Int) = + safeApiCall(errorMessageProvider) { diaryService.getDiaryTime(year, month, date) } - override suspend fun getMonthlyDiary(year: Int, month: Int): ApiResponse = - diaryService.getMonthlyDiary(year = year, month = month) + override suspend fun getMonthlyCalendarData(year: Int, month: Int) = + safeApiCall(errorMessageProvider) { diaryService.getMonthlyCalendarData(year, month) } - override suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse = - diaryService.getReplyDiary(year = year, month = month, date = date) + override suspend fun getMonthlyDiary(year: Int, month: Int) = + safeApiCall(errorMessageProvider) { diaryService.getMonthlyDiary(year, month) } - override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): ApiResponse = - diaryService.fetchDraftDiary(year = year, month = month, date = date) + override suspend fun getReplyDiary(year: Int, month: Int, date: Int) = + safeApiCall(errorMessageProvider) { diaryService.getReplyDiary(year, month, date) } - override suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse = - diaryService.saveDraftDiary(request) + override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int) = + safeApiCall(errorMessageProvider) { diaryService.fetchDraftDiary(year, month, date) } + + override suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto) = + safeApiCall(errorMessageProvider) { diaryService.saveDraftDiary(request) } } diff --git a/app/src/main/java/com/sopt/clody/data/remote/util/ApiError.kt b/app/src/main/java/com/sopt/clody/data/remote/util/ApiError.kt new file mode 100644 index 00000000..caf6a66b --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/remote/util/ApiError.kt @@ -0,0 +1,5 @@ +package com.sopt.clody.data.remote.util + +data class ApiError( + override val message: String, +) : Exception() diff --git a/app/src/main/java/com/sopt/clody/data/remote/util/SafeApiCall.kt b/app/src/main/java/com/sopt/clody/data/remote/util/SafeApiCall.kt new file mode 100644 index 00000000..612c20d2 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/remote/util/SafeApiCall.kt @@ -0,0 +1,25 @@ +package com.sopt.clody.data.remote.util + +import com.sopt.clody.data.remote.dto.base.ApiResponse +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider +import java.io.IOException +import kotlin.coroutines.cancellation.CancellationException + +suspend fun safeApiCall( + errorMessageProvider: ErrorMessageProvider, + action: suspend () -> ApiResponse, +): Result { + return try { + val response = action() + response.data?.let { Result.success(it) } + ?: Result.failure(ApiError(errorMessageProvider.getTemporaryError())) + } catch (exception: Throwable) { + if (exception is CancellationException) throw exception + + val error = when (exception) { + is IOException -> ApiError(errorMessageProvider.getNetworkError()) + else -> ApiError(errorMessageProvider.getTemporaryError()) + } + Result.failure(error) + } +} diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt index febe3024..653ceda0 100644 --- a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt @@ -2,101 +2,39 @@ package com.sopt.clody.data.repositoryimpl import com.sopt.clody.data.remote.datasource.DiaryRemoteDataSource import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto -import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto -import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto -import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto -import com.sopt.clody.data.remote.util.handleApiResponse -import com.sopt.clody.domain.model.DraftDiaryContents import com.sopt.clody.domain.repository.DiaryRepository -import com.sopt.clody.presentation.utils.network.ErrorMessages -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import retrofit2.HttpException import javax.inject.Inject class DiaryRepositoryImpl @Inject constructor( private val diaryRemoteDataSource: DiaryRemoteDataSource, ) : DiaryRepository { - override suspend fun writeDiary(date: String, content: List): Result = - runCatching { - diaryRemoteDataSource.writeDiary(date, content).handleApiResponse().getOrThrow() - } - override suspend fun deleteDailyDiary(year: Int, month: Int, day: Int): Result = - runCatching { - diaryRemoteDataSource.deleteDailyDiary(year, month, day).handleApiResponse().getOrThrow() - } + override suspend fun writeDiary(date: String, content: List) = + diaryRemoteDataSource.writeDiary(date, content) - override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): Result = - runCatching { - diaryRemoteDataSource.getDailyDiariesData(year, month, date).handleApiResponse() - }.getOrElse { exception -> - val errorMessage = when (exception) { - is HttpException -> { - when (exception.code()) { - in 400..499 -> ErrorMessages.FAILURE_TEMPORARY_MESSAGE - in 500..599 -> ErrorMessages.FAILURE_SERVER_MESSAGE - else -> ErrorMessages.UNKNOWN_ERROR - } - } - else -> exception.message ?: ErrorMessages.UNKNOWN_ERROR - } - Result.failure(Exception(errorMessage)) - } + override suspend fun deleteDailyDiary(year: Int, month: Int, day: Int) = + diaryRemoteDataSource.deleteDailyDiary(year, month, day) - override suspend fun getDiaryTime(year: Int, month: Int, date: Int): Result = - runCatching { - diaryRemoteDataSource.getDiaryTime(year, month, date).data - } + override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int) = + diaryRemoteDataSource.getDailyDiariesData(year, month, date) - override suspend fun getMonthlyCalendarData(year: Int, month: Int): Result = - runCatching { - diaryRemoteDataSource.getMonthlyCalendarData(year, month).handleApiResponse() - }.getOrElse { exception -> - val errorMessage = when (exception) { - is HttpException -> { - when (exception.code()) { - in 400..499 -> ErrorMessages.FAILURE_TEMPORARY_MESSAGE - in 500..599 -> ErrorMessages.FAILURE_SERVER_MESSAGE - else -> ErrorMessages.UNKNOWN_ERROR - } - } + override suspend fun getDiaryTime(year: Int, month: Int, date: Int) = + diaryRemoteDataSource.getDiaryTime(year, month, date) - else -> exception.message ?: ErrorMessages.UNKNOWN_ERROR - } - Result.failure(Exception(errorMessage)) - } + override suspend fun getMonthlyCalendarData(year: Int, month: Int) = + diaryRemoteDataSource.getMonthlyCalendarData(year, month) - override suspend fun getMonthlyDiary(year: Int, month: Int): Result = - runCatching { - diaryRemoteDataSource.getMonthlyDiary(year, month).handleApiResponse().getOrThrow() - } + override suspend fun getMonthlyDiary(year: Int, month: Int) = + diaryRemoteDataSource.getMonthlyDiary(year, month) - override suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result = - runCatching { - val response = diaryRemoteDataSource.getReplyDiary(year, month, date).data - if (response.content == null) { - throw IllegalStateException(FAILURE_TEMPORARY_MESSAGE) - } - response - } + override suspend fun getReplyDiary(year: Int, month: Int, date: Int) = + diaryRemoteDataSource.getReplyDiary(year, month, date) - override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result = - runCatching { - diaryRemoteDataSource - .fetchDraftDiary(year, month, date) - .handleApiResponse() - .getOrThrow() - .toDomain() - } + override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int) = + diaryRemoteDataSource.fetchDraftDiary(year, month, date).map { it.toDomain() } - override suspend fun saveDraftDiary(date: String, contents: List): Result = - runCatching { - diaryRemoteDataSource - .saveDraftDiary(SaveDraftDiaryRequestDto(date = date, draftDiaries = contents)) - .handleApiResponse() - .getOrThrow() - } + override suspend fun saveDraftDiary(date: String, contents: List): Result { + val request = SaveDraftDiaryRequestDto(date, contents) + return diaryRemoteDataSource.saveDraftDiary(request) + } } diff --git a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt index 3666ed18..d31d8a84 100644 --- a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt +++ b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt @@ -1,5 +1,6 @@ package com.sopt.clody.domain.repository +import com.sopt.clody.data.remote.dto.base.ApiResponse import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto diff --git a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt index 4829638c..a85085a4 100644 --- a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt +++ b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt @@ -10,37 +10,38 @@ import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto +import com.sopt.clody.data.remote.util.ApiError class FakeDiaryRemoteDataSource : DiaryRemoteDataSource { var draftDiariesResponse: ApiResponse? = null var saveDraftResponse: ApiResponse? = null - override suspend fun writeDiary(date: String, content: List): ApiResponse { + override suspend fun writeDiary(date: String, content: List): Result { throw NotImplementedError() } - override suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): ApiResponse { + override suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): Result { throw NotImplementedError() } - override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): ApiResponse { + override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): Result { throw NotImplementedError() } - override suspend fun getDiaryTime(year: Int, month: Int, date: Int): ApiResponse { + override suspend fun getDiaryTime(year: Int, month: Int, date: Int): Result { throw NotImplementedError() } - override suspend fun getMonthlyCalendarData(year: Int, month: Int): ApiResponse { + override suspend fun getMonthlyCalendarData(year: Int, month: Int): Result { throw NotImplementedError() } - override suspend fun getMonthlyDiary(year: Int, month: Int): ApiResponse { + override suspend fun getMonthlyDiary(year: Int, month: Int): Result { throw NotImplementedError() } - override suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse { + override suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result { throw NotImplementedError() } @@ -48,16 +49,16 @@ class FakeDiaryRemoteDataSource : DiaryRemoteDataSource { year: Int, month: Int, date: Int, - ): ApiResponse { - return draftDiariesResponse - ?: throw IllegalStateException("draftDiariesResponse not set") + ): Result { + return draftDiariesResponse?.let { Result.success(it.data!!) } + ?: Result.failure(ApiError("draftDiariesResponse not set")) } override suspend fun saveDraftDiary( request: SaveDraftDiaryRequestDto, - ): ApiResponse { - return saveDraftResponse - ?: throw IllegalStateException("saveDraftResponse not set") + ): Result { + return saveDraftResponse?.let { Result.success(Unit) } + ?: Result.failure(ApiError("saveDraftResponse not set")) } fun setDraftDiariesResponse(list: List) { From b48ca20076c84a76b591ae0e542abd18865f7f18 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 4 Aug 2025 02:08:09 +0900 Subject: [PATCH 291/299] [CHORE/#297] ktlint Format --- .../sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt | 1 - .../java/com/sopt/clody/domain/repository/DiaryRepository.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt index b8e50eb1..457471f1 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt @@ -1,6 +1,5 @@ package com.sopt.clody.data.remote.datasource -import com.sopt.clody.data.remote.dto.base.ApiResponse import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto diff --git a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt index c51679f9..0ab3ff7f 100644 --- a/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt +++ b/app/src/main/java/com/sopt/clody/domain/repository/DiaryRepository.kt @@ -1,6 +1,5 @@ package com.sopt.clody.domain.repository -import com.sopt.clody.data.remote.dto.base.ApiResponse import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto From d71ab4811dbfa2b308c72d67a287461a906b0672 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 4 Aug 2025 03:21:09 +0900 Subject: [PATCH 292/299] =?UTF-8?q?[CHORE/#297]=20CI=20=EB=B9=8C=EB=93=9C?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt index fa00f549..56e43446 100644 --- a/app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt +++ b/app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt @@ -6,6 +6,6 @@ import javax.inject.Inject class WriteDiaryUseCase @Inject constructor( private val diaryRepository: DiaryRepository, ) { - suspend operator fun invoke(date: String, content: List) = - diaryRepository.writeDiary(date, content) + suspend operator fun invoke(lang: String, date: String, content: List) = + diaryRepository.writeDiary(lang, date, content) } From 571e58735d54401deeef8398056eddbe8588b477 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 4 Aug 2025 04:45:41 +0900 Subject: [PATCH 293/299] =?UTF-8?q?[FIX/#312]=20CD=20=EB=B9=8C=EB=93=9C=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 --- .github/workflows/android_cd.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 12d8a49b..3cab7083 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -45,12 +45,18 @@ jobs: - name: Install Firebase CLI run: curl -sL https://firebase.tools | bash - # google-services.json - - name: Decode google-services.json + # google-services.json (prod) + - name: Decode Prod google-services.json env: FIREBASE_SECRET: ${{ secrets.FIREBASE_SECRET }} run: echo $FIREBASE_SECRET | base64 --decode > app/google-services.json + # google-services.json (debug) + - name: Decode Debug google-services.json + env: + FIREBASE_SECRET: ${{ secrets.FIREBASE_DEBUG_SECRET }} + run: echo $FIREBASE_DEBUG_SECRET | base64 --decode > app/src/debug/google-services.json + # keystore 복호화 - name: Decode keystore file env: @@ -67,6 +73,7 @@ jobs: AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} GOOGLE_ADMOB_APP_ID: ${{ secrets.GOOGLE_ADMOB_APP_ID }} GOOGLE_ADMOB_UNIT_ID: ${{ secrets.GOOGLE_ADMOB_UNIT_ID }} + GOOGLE_AUTH_WEB_CLIENT_ID: ${{ secrets.GOOGLE_AUTH_WEB_CLIENT_ID }} STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} @@ -77,6 +84,7 @@ jobs: echo "amplitude.api.key=$AMPLITUDE_API_KEY" >> local.properties echo "GOOGLE_ADMOB_APP_ID=$GOOGLE_ADMOB_APP_ID" >> local.properties echo "GOOGLE_ADMOB_UNIT_ID=$GOOGLE_ADMOB_UNIT_ID" >> local.properties + echo "GOOGLE_AUTH_WEB_CLIENT_ID" >> local.properties echo "storeFile=keystore/clody_release.jks" >> local.properties echo "storePassword=$STORE_PASSWORD" >> local.properties echo "keyAlias=$KEY_ALIAS" >> local.properties From 7ddeb876a05db1c9c19da84eee7b7b25d6b75fe4 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 4 Aug 2025 05:06:42 +0900 Subject: [PATCH 294/299] =?UTF-8?q?[FIX/#312]=20CD=20=EB=B9=8C=EB=93=9C=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 --- .github/workflows/android_cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 3cab7083..1f1e972d 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -54,7 +54,7 @@ jobs: # google-services.json (debug) - name: Decode Debug google-services.json env: - FIREBASE_SECRET: ${{ secrets.FIREBASE_DEBUG_SECRET }} + FIREBASE_DEBUG_SECRET: ${{ secrets.FIREBASE_DEBUG_SECRET }} run: echo $FIREBASE_DEBUG_SECRET | base64 --decode > app/src/debug/google-services.json # keystore 복호화 From d9367b84d8f0a133f986997a01314eadf7f06313 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 4 Aug 2025 05:20:22 +0900 Subject: [PATCH 295/299] =?UTF-8?q?[FIX/#312]=20CD=20=EB=B9=8C=EB=93=9C=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 --- .github/workflows/android_cd.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 1f1e972d..1e1a56ca 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -84,7 +84,7 @@ jobs: echo "amplitude.api.key=$AMPLITUDE_API_KEY" >> local.properties echo "GOOGLE_ADMOB_APP_ID=$GOOGLE_ADMOB_APP_ID" >> local.properties echo "GOOGLE_ADMOB_UNIT_ID=$GOOGLE_ADMOB_UNIT_ID" >> local.properties - echo "GOOGLE_AUTH_WEB_CLIENT_ID" >> local.properties + echo "GOOGLE_AUTH_WEB_CLIENT_ID=$GOOGLE_AUTH_WEB_CLIENT_ID" >> local.properties echo "storeFile=keystore/clody_release.jks" >> local.properties echo "storePassword=$STORE_PASSWORD" >> local.properties echo "keyAlias=$KEY_ALIAS" >> local.properties @@ -92,7 +92,20 @@ jobs: # Debug APK 빌드 - name: Build Debug APK - run: ./gradlew assembleDebug --stacktrace + run: | + echo "🔨 Debug APK 빌드 시작..." + echo "📁 현재 디렉토리: $(pwd)" + echo "📁 파일 목록:" + ls -la + echo "" + echo "📁 app 디렉토리:" + ls -la app/ + echo "" + echo "📁 google-services.json 확인:" + ls -la app/google-services.json app/src/debug/google-services.json 2>/dev/null || echo "일부 파일이 없습니다" + echo "" + echo "🚀 Gradle 빌드 시작..." + ./gradlew assembleDebug --stacktrace # Set up Firebase Service Account Credentials - name: Set up Firebase Service Account Credentials From 9e73b8912ae1e206fb449f2a00dff27211a3faa3 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 4 Aug 2025 05:39:40 +0900 Subject: [PATCH 296/299] =?UTF-8?q?[FIX/#312]=20CD=20=EB=B9=8C=EB=93=9C=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 --- .github/workflows/android_cd.yml | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 1e1a56ca..9322d3a4 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -68,7 +68,8 @@ jobs: # local.properties - name: Generate local.properties env: - CLODY_BASE_URL: ${{ secrets.CLODY_TEST_URL }} + CLODY_BASE_URL: ${{ secrets.CLODY_BASE_URL }} + CLODY_TEST_URL: ${{ secrets.CLODY_TEST_URL }} KAKAO_API_KEY: ${{ secrets.KAKAO_API_KEY }} AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }} GOOGLE_ADMOB_APP_ID: ${{ secrets.GOOGLE_ADMOB_APP_ID }} @@ -79,7 +80,7 @@ jobs: KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} run: | echo "clody.base.url=$CLODY_BASE_URL" >> local.properties - echo "clody.test.url=$CLODY_BASE_URL" >> local.properties + echo "clody.test.url=$CLODY_TEST_URL" >> local.properties echo "kakao.api.key=$KAKAO_API_KEY" >> local.properties echo "amplitude.api.key=$AMPLITUDE_API_KEY" >> local.properties echo "GOOGLE_ADMOB_APP_ID=$GOOGLE_ADMOB_APP_ID" >> local.properties @@ -92,20 +93,7 @@ jobs: # Debug APK 빌드 - name: Build Debug APK - run: | - echo "🔨 Debug APK 빌드 시작..." - echo "📁 현재 디렉토리: $(pwd)" - echo "📁 파일 목록:" - ls -la - echo "" - echo "📁 app 디렉토리:" - ls -la app/ - echo "" - echo "📁 google-services.json 확인:" - ls -la app/google-services.json app/src/debug/google-services.json 2>/dev/null || echo "일부 파일이 없습니다" - echo "" - echo "🚀 Gradle 빌드 시작..." - ./gradlew assembleDebug --stacktrace + run: ./gradlew assembleDebug --stacktrace # Set up Firebase Service Account Credentials - name: Set up Firebase Service Account Credentials From b3825692ecfd240c9dffbe7d664d7b8ff21bc218 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 4 Aug 2025 05:53:15 +0900 Subject: [PATCH 297/299] =?UTF-8?q?[FIX/#312]=20CD=20=EB=B9=8C=EB=93=9C=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 --- .github/workflows/android_cd.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 9322d3a4..683fd360 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -122,13 +122,13 @@ jobs: - name: Upload APK to Firebase App Distribution env: GOOGLE_APPLICATION_CREDENTIALS: $HOME/firebase-credentials.json - FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }} + FIREBASE_APP_ID: ${{ secrets.FIREBASE_DEBUG_APP_ID }} run: | - echo "🔥 FIREBASE_APP_ID 확인: $FIREBASE_APP_ID" + echo "🔥 FIREBASE_DEBUG_APP_ID 확인: $FIREBASE_DEBUG_APP_ID" - # 만약 FIREBASE_APP_ID가 없으면 에러 출력 후 종료 - if [ -z "$FIREBASE_APP_ID" ]; then - echo "❌ ERROR: FIREBASE_APP_ID가 설정되지 않았습니다. GitHub Secrets에서 확인하세요." + # 만약 FIREBASE_DEBUG_APP_ID가 없으면 에러 출력 후 종료 + if [ -z "$FIREBASE_DEBUG_APP_ID" ]; then + echo "❌ ERROR: FIREBASE_DEBUG_APP_ID가 설정되지 않았습니다. GitHub Secrets에서 확인하세요." exit 1 fi @@ -137,7 +137,7 @@ jobs: echo "GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS" firebase appdistribution:distribute app/build/outputs/apk/debug/app-debug.apk \ - --app "$FIREBASE_APP_ID" \ + --app "$FIREBASE_DEBUG_APP_ID" \ --release-notes "🍀 새로운 테스트 버전이 업로드되었습니다~" \ --groups "clody-tester-group" From 3807267c7aa33677a9d2f94edfeb44ce52719480 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 4 Aug 2025 06:05:42 +0900 Subject: [PATCH 298/299] =?UTF-8?q?[FIX/#312]=20CD=20=EB=B9=8C=EB=93=9C=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 --- .github/workflows/android_cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android_cd.yml b/.github/workflows/android_cd.yml index 683fd360..27ba113d 100644 --- a/.github/workflows/android_cd.yml +++ b/.github/workflows/android_cd.yml @@ -122,7 +122,7 @@ jobs: - name: Upload APK to Firebase App Distribution env: GOOGLE_APPLICATION_CREDENTIALS: $HOME/firebase-credentials.json - FIREBASE_APP_ID: ${{ secrets.FIREBASE_DEBUG_APP_ID }} + FIREBASE_DEBUG_APP_ID: ${{ secrets.FIREBASE_DEBUG_APP_ID }} run: | echo "🔥 FIREBASE_DEBUG_APP_ID 확인: $FIREBASE_DEBUG_APP_ID" From 858e7effc184a14e2c90bd38bab94284be5cd486 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 4 Aug 2025 16:58:59 +0900 Subject: [PATCH 299/299] =?UTF-8?q?[CHORE/#312]=20FakeDiaryRemoteDataSourc?= =?UTF-8?q?e=20writeDiary=20=ED=95=A8=EC=88=98=20=EB=A7=A4=EA=B0=9C?= =?UTF-8?q?=EB=B3=80=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 --- .../java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt index a85085a4..0ab3fe21 100644 --- a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt +++ b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt @@ -17,7 +17,7 @@ class FakeDiaryRemoteDataSource : DiaryRemoteDataSource { var draftDiariesResponse: ApiResponse? = null var saveDraftResponse: ApiResponse? = null - override suspend fun writeDiary(date: String, content: List): Result { + override suspend fun writeDiary(lang: String, date: String, content: List): Result { throw NotImplementedError() }