From f54c3bf19318fe4190d74db5bb9d11eb4d6c702b Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 19:23:03 +0900 Subject: [PATCH 01/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20:feature:goal-editor?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/goal-editor/.gitignore | 1 + feature/goal-editor/build.gradle.kts | 7 ++++++ feature/goal-editor/consumer-rules.pro | 0 feature/goal-editor/proguard-rules.pro | 21 ++++++++++++++++ .../goal_editor/ExampleInstrumentedTest.kt | 24 +++++++++++++++++++ .../goal-editor/src/main/AndroidManifest.xml | 4 ++++ .../com/twix/goal_editor/ExampleUnitTest.kt | 17 +++++++++++++ settings.gradle.kts | 1 + 8 files changed, 75 insertions(+) create mode 100644 feature/goal-editor/.gitignore create mode 100644 feature/goal-editor/build.gradle.kts create mode 100644 feature/goal-editor/consumer-rules.pro create mode 100644 feature/goal-editor/proguard-rules.pro create mode 100644 feature/goal-editor/src/androidTest/java/com/twix/goal_editor/ExampleInstrumentedTest.kt create mode 100644 feature/goal-editor/src/main/AndroidManifest.xml create mode 100644 feature/goal-editor/src/test/java/com/twix/goal_editor/ExampleUnitTest.kt diff --git a/feature/goal-editor/.gitignore b/feature/goal-editor/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/goal-editor/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/goal-editor/build.gradle.kts b/feature/goal-editor/build.gradle.kts new file mode 100644 index 00000000..0441eb17 --- /dev/null +++ b/feature/goal-editor/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + alias(libs.plugins.twix.feature) +} + +android { + namespace = "com.twix.goal-editor" +} diff --git a/feature/goal-editor/consumer-rules.pro b/feature/goal-editor/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/feature/goal-editor/proguard-rules.pro b/feature/goal-editor/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/goal-editor/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/goal-editor/src/androidTest/java/com/twix/goal_editor/ExampleInstrumentedTest.kt b/feature/goal-editor/src/androidTest/java/com/twix/goal_editor/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..28318cea --- /dev/null +++ b/feature/goal-editor/src/androidTest/java/com/twix/goal_editor/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.twix.goal_editor + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.twix.goal_editor.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/goal-editor/src/main/AndroidManifest.xml b/feature/goal-editor/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/goal-editor/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/goal-editor/src/test/java/com/twix/goal_editor/ExampleUnitTest.kt b/feature/goal-editor/src/test/java/com/twix/goal_editor/ExampleUnitTest.kt new file mode 100644 index 00000000..515ebb1e --- /dev/null +++ b/feature/goal-editor/src/test/java/com/twix/goal_editor/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.twix.goal_editor + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 29626c00..70ee5aa1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,3 +36,4 @@ include(":core:design-system") include(":core:network") include(":core:analytics") include(":feature:main") +include(":feature:goal-editor") From f75d8657c8945e82c3f7e35222315843e7ffaa73 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:03:41 +0900 Subject: [PATCH 02/43] =?UTF-8?q?=F0=9F=94=A5=20Remove:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=ED=8C=8C=EC=9D=BC=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 --- .../goal_editor/ExampleInstrumentedTest.kt | 24 ------------------- .../com/twix/goal_editor/ExampleUnitTest.kt | 17 ------------- 2 files changed, 41 deletions(-) delete mode 100644 feature/goal-editor/src/androidTest/java/com/twix/goal_editor/ExampleInstrumentedTest.kt delete mode 100644 feature/goal-editor/src/test/java/com/twix/goal_editor/ExampleUnitTest.kt diff --git a/feature/goal-editor/src/androidTest/java/com/twix/goal_editor/ExampleInstrumentedTest.kt b/feature/goal-editor/src/androidTest/java/com/twix/goal_editor/ExampleInstrumentedTest.kt deleted file mode 100644 index 28318cea..00000000 --- a/feature/goal-editor/src/androidTest/java/com/twix/goal_editor/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.twix.goal_editor - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.twix.goal_editor.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/feature/goal-editor/src/test/java/com/twix/goal_editor/ExampleUnitTest.kt b/feature/goal-editor/src/test/java/com/twix/goal_editor/ExampleUnitTest.kt deleted file mode 100644 index 515ebb1e..00000000 --- a/feature/goal-editor/src/test/java/com/twix/goal_editor/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.twix.goal_editor - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file From 354c38499db90b94e55d42cffc9caa11d3b381ca Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:04:16 +0900 Subject: [PATCH 03/43] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20=EB=A6=AC?= =?UTF-8?q?=EC=86=8C=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 --- .../src/main/res/drawable/ic_arrow3_left.xml | 13 ++++++++++ .../res/drawable/ic_chevron_down_circle.xml | 16 ++++++++++++ .../src/main/res/drawable/ic_emoji_add.xml | 25 +++++++++++++++++++ .../src/main/res/values/strings.xml | 13 ++++++++++ 4 files changed, 67 insertions(+) create mode 100644 core/design-system/src/main/res/drawable/ic_arrow3_left.xml create mode 100644 core/design-system/src/main/res/drawable/ic_chevron_down_circle.xml create mode 100644 core/design-system/src/main/res/drawable/ic_emoji_add.xml diff --git a/core/design-system/src/main/res/drawable/ic_arrow3_left.xml b/core/design-system/src/main/res/drawable/ic_arrow3_left.xml new file mode 100644 index 00000000..89be7347 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_arrow3_left.xml @@ -0,0 +1,13 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_chevron_down_circle.xml b/core/design-system/src/main/res/drawable/ic_chevron_down_circle.xml new file mode 100644 index 00000000..9a59f25e --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_chevron_down_circle.xml @@ -0,0 +1,16 @@ + + + + diff --git a/core/design-system/src/main/res/drawable/ic_emoji_add.xml b/core/design-system/src/main/res/drawable/ic_emoji_add.xml new file mode 100644 index 00000000..9b6007b1 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_emoji_add.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 8c344190..cf234fa9 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -7,6 +7,10 @@ 오늘 완료 + 매일 + 매주 + 매월 + @@ -18,5 +22,14 @@ 오늘 우리 목표 첫 목표를 세워볼까요? + + + 목표 직접 만들기 + 목표 수정하기 + 목표를 입력해 보세요. + 반복 주기 + 시작 날짜 + 종료 날짜 + 종료 날짜 설정 \ No newline at end of file From 2d4e4b76e8f1d59361882a26376334b47a3e5f0c Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:04:54 +0900 Subject: [PATCH 04/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20namespac?= =?UTF-8?q?e=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/goal-editor/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/goal-editor/build.gradle.kts b/feature/goal-editor/build.gradle.kts index 0441eb17..c8916ac6 100644 --- a/feature/goal-editor/build.gradle.kts +++ b/feature/goal-editor/build.gradle.kts @@ -3,5 +3,5 @@ plugins { } android { - namespace = "com.twix.goal-editor" + namespace = "com.twix.goal_editor" } From 40df7b9676cc305bbfb54707fc2cbc1eddfe625e Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:05:27 +0900 Subject: [PATCH 05/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20GoalEditorModule=20Ko?= =?UTF-8?q?in=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + .../main/java/com/yapp/twix/di/FeatureModules.kt | 2 ++ .../com/twix/goal_editor/di/GoalEditorModule.kt | 15 +++++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/di/GoalEditorModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 53d48767..ab17c7f3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.domain) implementation(projects.feature.login) implementation(projects.feature.main) + implementation(projects.feature.goalEditor) // Firebase implementation(platform(libs.google.firebase.bom)) diff --git a/app/src/main/java/com/yapp/twix/di/FeatureModules.kt b/app/src/main/java/com/yapp/twix/di/FeatureModules.kt index 7365a495..28d0a1e2 100644 --- a/app/src/main/java/com/yapp/twix/di/FeatureModules.kt +++ b/app/src/main/java/com/yapp/twix/di/FeatureModules.kt @@ -1,5 +1,6 @@ package com.yapp.twix.di +import com.twix.goal_editor.di.goalEditorModule import com.twix.home.di.homeModule import com.twix.login.di.loginModule import com.twix.main.di.mainModule @@ -10,4 +11,5 @@ val featureModules: List = loginModule, mainModule, homeModule, + goalEditorModule, ) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/di/GoalEditorModule.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/di/GoalEditorModule.kt new file mode 100644 index 00000000..f3c8dc17 --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/di/GoalEditorModule.kt @@ -0,0 +1,15 @@ +package com.twix.goal_editor.di + +import com.twix.goal_editor.GoalEditorViewModel +import com.twix.goal_editor.navigation.GoalEditorNavGraph +import com.twix.navigation.NavRoutes +import com.twix.navigation.base.NavGraphContributor +import org.koin.core.module.dsl.viewModelOf +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val goalEditorModule = + module { + viewModelOf(::GoalEditorViewModel) + single(named(NavRoutes.GoalEditorGraph.route)) { GoalEditorNavGraph } + } From 36d8457c488baae1c21452c8766d4c19fa3ba7ec Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:05:44 +0900 Subject: [PATCH 06/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20GoalEditorNavGraph=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 --- .../navigation/GoalEditorNavGraph.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/navigation/GoalEditorNavGraph.kt diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/navigation/GoalEditorNavGraph.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/navigation/GoalEditorNavGraph.kt new file mode 100644 index 00000000..3de9e41c --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/navigation/GoalEditorNavGraph.kt @@ -0,0 +1,29 @@ +package com.twix.goal_editor.navigation + +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.twix.goal_editor.GoalEditorRoute +import com.twix.navigation.NavRoutes +import com.twix.navigation.base.NavGraphContributor + +object GoalEditorNavGraph : NavGraphContributor { + override val graphRoute: NavRoutes + get() = NavRoutes.GoalEditorGraph + override val startDestination: String + get() = NavRoutes.GoalEditorRoute.route + + override fun NavGraphBuilder.registerGraph(navController: NavHostController) { + navigation( + route = graphRoute.route, + startDestination = startDestination, + ) { + composable(NavRoutes.GoalEditorRoute.route) { + GoalEditorRoute( + navigateToBack = navController::popBackStack, + ) + } + } + } +} From cfb702fd64192a231dc4b48f0dbf2d8ea1d59afc Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:06:05 +0900 Subject: [PATCH 07/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20EmojiPicker=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 --- .../twix/goal_editor/component/EmojiPicker.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/component/EmojiPicker.kt diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/EmojiPicker.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/EmojiPicker.kt new file mode 100644 index 00000000..69931ba5 --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/EmojiPicker.kt @@ -0,0 +1,46 @@ +package com.twix.goal_editor.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +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.twix.designsystem.R +import com.twix.designsystem.theme.GrayColor +import com.twix.ui.extension.noRippleClickable + +@Composable +fun EmojiPicker( + emojiId: Long, + onClick: () -> Unit, +) { + Box( + modifier = + Modifier + .clip(CircleShape) + .background(GrayColor.C050) + .border(1.dp, GrayColor.C300, CircleShape) + .padding(26.dp) + .noRippleClickable(onClick = onClick), + ) { + Image( + painter = + if (emojiId == -1L) { + painterResource(R.drawable.ic_emoji_add) + } else { + painterResource(R.drawable.ic_emoji_add) + }, + contentDescription = "emoji", + modifier = + Modifier + .size(56.dp), + ) + } +} From 38cf7367f5d45412b3107c2a181e625c549ddbe6 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:07:16 +0900 Subject: [PATCH 08/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20GoalEditorTopBar=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 --- .../goal_editor/component/GoalEditorTopBar.kt | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalEditorTopBar.kt diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalEditorTopBar.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalEditorTopBar.kt new file mode 100644 index 00000000..83cfe4b6 --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalEditorTopBar.kt @@ -0,0 +1,87 @@ +package com.twix.goal_editor.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.material3.HorizontalDivider +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.theme.GrayColor +import com.twix.domain.model.enums.AppTextStyle +import com.twix.ui.extension.noRippleClickable + +@Composable +fun GoalEditorTopBar( + isEdit: Boolean = false, + onBack: () -> Unit, +) { + val title = + if (isEdit) { + stringResource(R.string.goal_editor_title_edit) + } else { + stringResource(R.string.goal_editor_title) + } + + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 10.dp), + ) { + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + + Row( + modifier = + Modifier + .fillMaxWidth() + .height(60.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(R.drawable.ic_arrow3_left), + contentDescription = "back", + modifier = + Modifier + .padding(18.dp) + .size(24.dp) + .noRippleClickable(onClick = onBack), + ) + + VerticalDivider(thickness = 1.dp, color = GrayColor.C500) + + AppText( + text = title, + style = AppTextStyle.H4Brand, + color = GrayColor.C500, + textAlign = TextAlign.Center, + modifier = + Modifier + .weight(1f), + ) + + VerticalDivider(thickness = 1.dp, color = GrayColor.C500) + + Box( + modifier = + Modifier + .padding(18.dp) + .size(24.dp), + ) + } + + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + } +} From bd53b77fc2e53a44e346933778933728e41211ca Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:07:33 +0900 Subject: [PATCH 09/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20GoalEditorUiState=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 --- .../goal_editor/model/GoalEditorUiState.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt new file mode 100644 index 00000000..1ba03685 --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt @@ -0,0 +1,19 @@ +package com.twix.goal_editor.model + +import androidx.compose.runtime.Immutable +import com.twix.domain.model.enums.RepeatType +import com.twix.ui.base.State +import java.time.LocalDate + +@Immutable +data class GoalEditorUiState( + val selectedIconId: Long = -1L, + val goalTitle: String = "", + val selectedRepeatType: RepeatType = RepeatType.DAILY, + val repeatCount: Int = 0, + val startDate: LocalDate = LocalDate.now(), + val endDate: LocalDate? = null, +) : State { + val isEnabled: Boolean + get() = selectedIconId != -1L && goalTitle.isNotBlank() && repeatCount > 0 +} From 5024ce6606543e010404d28bd686acaa22b343cf Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:07:49 +0900 Subject: [PATCH 10/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20RepeatType=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/twix/domain/model/enums/RepeatType.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 domain/src/main/java/com/twix/domain/model/enums/RepeatType.kt diff --git a/domain/src/main/java/com/twix/domain/model/enums/RepeatType.kt b/domain/src/main/java/com/twix/domain/model/enums/RepeatType.kt new file mode 100644 index 00000000..61a44e25 --- /dev/null +++ b/domain/src/main/java/com/twix/domain/model/enums/RepeatType.kt @@ -0,0 +1,7 @@ +package com.twix.domain.model.enums + +enum class RepeatType { + DAILY, + WEEKLY, + MONTHLY, +} From 9a6f83d07a4c2f01d75393d20d7012716186d17d Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:08:08 +0900 Subject: [PATCH 11/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EA=B3=B5=ED=86=B5=20=EC=8A=A4=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/CommonSwitch.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 core/design-system/src/main/java/com/twix/designsystem/components/common/CommonSwitch.kt diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/common/CommonSwitch.kt b/core/design-system/src/main/java/com/twix/designsystem/components/common/CommonSwitch.kt new file mode 100644 index 00000000..ac156819 --- /dev/null +++ b/core/design-system/src/main/java/com/twix/designsystem/components/common/CommonSwitch.kt @@ -0,0 +1,61 @@ +package com.twix.designsystem.components.common + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.twix.designsystem.theme.CommonColor +import com.twix.designsystem.theme.GrayColor +import kotlin.math.roundToInt + +@Composable +fun CommonSwitch( + modifier: Modifier = Modifier, + checked: Boolean, + onClick: (Boolean) -> Unit, +) { + val density = LocalDensity.current + val minBound = with(density) { 0.dp.toPx() } + val maxBound = with(density) { 18.dp.toPx() } + val state by animateFloatAsState( + targetValue = if (checked) maxBound else minBound, + animationSpec = tween(durationMillis = 500), + label = "common switch", + ) + + Box( + modifier = + modifier + .size(width = 48.dp, height = 30.dp) + .clip(RoundedCornerShape(999.dp)) + .background(if (checked) GrayColor.C500 else CommonColor.White) + .border(1.dp, GrayColor.C500, RoundedCornerShape(999.dp)) + .clickable(onClick = { onClick(!checked) }), + contentAlignment = Alignment.CenterStart, + ) { + Box( + modifier = + Modifier + .offset { IntOffset(state.roundToInt(), 0) } + .padding(4.dp) + .size(22.dp) + .clip(CircleShape) + .background(if (checked) CommonColor.White else GrayColor.C500), + ) + } +} From f4ec675bd041a924202b860b8e8ffa8837377a89 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:08:27 +0900 Subject: [PATCH 12/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EA=B3=B5=ED=86=B5=20UnderlineTextField=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 --- .../text_field/UnderlineTextField.kt | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt b/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt new file mode 100644 index 00000000..790de3ce --- /dev/null +++ b/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt @@ -0,0 +1,77 @@ +package com.twix.designsystem.components.text_field + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.theme.GrayColor +import com.twix.designsystem.theme.LocalAppTypography +import com.twix.designsystem.theme.toTextStyle +import com.twix.domain.model.enums.AppTextStyle + +@Composable +fun UnderlineTextField( + modifier: Modifier = Modifier, + value: String, + placeHolder: String = "", + textStyle: AppTextStyle = AppTextStyle.T2, + enabled: Boolean = true, + readOnly: Boolean = false, + singleLine: Boolean = true, + maxLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + onValueChange: (String) -> Unit, +) { + val typo = LocalAppTypography.current + + Column( + horizontalAlignment = Alignment.Start, + ) { + Spacer(Modifier.height(4.dp)) + + Box( + modifier = + modifier + .padding(horizontal = 8.dp, vertical = 10.dp), + contentAlignment = Alignment.CenterStart, + ) { + if (value.isBlank()) { + AppText( + text = placeHolder, + style = textStyle, + color = GrayColor.C200, + modifier = + Modifier + .align(Alignment.CenterStart), + ) + } + + BasicTextField( + value = value, + textStyle = textStyle.toTextStyle(typo).copy(color = GrayColor.C500), + onValueChange = onValueChange, + enabled = enabled, + readOnly = readOnly, + singleLine = singleLine, + maxLines = maxLines, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + } + + Spacer(Modifier.height(4.dp)) + + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500, modifier = modifier) + } +} From 5cec265b42b004aa506a7aaebf51d328f8169151 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:08:49 +0900 Subject: [PATCH 13/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=B9=88=EA=B3=B5?= =?UTF-8?q?=EA=B0=84=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=ED=8F=AC=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=20=ED=95=B4=EC=A0=9C=ED=95=98=EB=8A=94=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=20=ED=99=95=EC=9E=A5=20=EB=A9=94=EC=84=9C=EB=93=9C=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 --- .../twix/ui/extension/DismissKeyboardOnTap.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 core/ui/src/main/java/com/twix/ui/extension/DismissKeyboardOnTap.kt diff --git a/core/ui/src/main/java/com/twix/ui/extension/DismissKeyboardOnTap.kt b/core/ui/src/main/java/com/twix/ui/extension/DismissKeyboardOnTap.kt new file mode 100644 index 00000000..5303b5ba --- /dev/null +++ b/core/ui/src/main/java/com/twix/ui/extension/DismissKeyboardOnTap.kt @@ -0,0 +1,29 @@ +package com.twix.ui.extension + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController + +@Composable +fun Modifier.dismissKeyboardOnTap( + enabled: Boolean = true, + onDismiss: () -> Unit = {}, +): Modifier { + if (!enabled) return this + + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current + + return this.pointerInput(Unit) { + detectTapGestures( + onTap = { + focusManager.clearFocus() + keyboardController?.hide() + onDismiss() + }, + ) + } +} From 871786a785367f2457f26d351101da5ed9bb1d4d Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:09:06 +0900 Subject: [PATCH 14/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20GoalEditorIntent=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/twix/goal_editor/GoalEditorIntent.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt new file mode 100644 index 00000000..ec8206ef --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt @@ -0,0 +1,33 @@ +package com.twix.goal_editor + +import com.twix.domain.model.enums.RepeatType +import com.twix.ui.base.Intent +import java.time.LocalDate + +sealed interface GoalEditorIntent : Intent { + data class SelectIcon( + val iconId: Long, + ) : GoalEditorIntent + + data class UpdateTitle( + val title: String, + ) : GoalEditorIntent + + data class UpdateRepeatType( + val repeatType: RepeatType, + ) : GoalEditorIntent + + data class UpdateRepeatCount( + val repeatCount: Int, + ) : GoalEditorIntent + + data class UpdateStartDate( + val startDate: LocalDate, + ) : GoalEditorIntent + + data class UpdateEndDate( + val endDate: LocalDate, + ) : GoalEditorIntent + + data object Save : GoalEditorIntent +} From dc870cae2ef3addedf5c837dc1c779fa6a6cf7cd Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:09:29 +0900 Subject: [PATCH 15/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20Intent,=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=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 --- .../twix/goal_editor/GoalEditorViewModel.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt new file mode 100644 index 00000000..a88a29ac --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt @@ -0,0 +1,57 @@ +package com.twix.goal_editor + +import com.twix.domain.model.enums.RepeatType +import com.twix.goal_editor.model.GoalEditorUiState +import com.twix.ui.base.BaseViewModel +import java.time.LocalDate + +class GoalEditorViewModel : + BaseViewModel( + GoalEditorUiState(), + ) { + override suspend fun handleIntent(intent: GoalEditorIntent) { + when (intent) { + GoalEditorIntent.Save -> save() + is GoalEditorIntent.SelectIcon -> selectIcon(intent.iconId) + is GoalEditorIntent.UpdateEndDate -> updateEndDate(intent.endDate) + is GoalEditorIntent.UpdateRepeatCount -> updateRepeatCount(intent.repeatCount) + is GoalEditorIntent.UpdateRepeatType -> updateRepeatType(intent.repeatType) + is GoalEditorIntent.UpdateStartDate -> updateStartDate(intent.startDate) + is GoalEditorIntent.UpdateTitle -> updateTitle(intent.title) + } + } + + private fun selectIcon(iconId: Long) { + if (iconId <= 0) return + + reduce { copy(selectedIconId = iconId) } + } + + private fun updateTitle(title: String) { + if (title.isBlank()) return + + reduce { copy(goalTitle = title) } + } + + private fun updateRepeatType(repeatType: RepeatType) { + reduce { copy(selectedRepeatType = repeatType) } + } + + private fun updateRepeatCount(repeatCount: Int) { + if (repeatCount <= 0) return + + reduce { copy(repeatCount = repeatCount) } + } + + private fun updateStartDate(startDate: LocalDate) { + reduce { copy(startDate = startDate) } + } + + private fun updateEndDate(endDate: LocalDate) { + reduce { copy(endDate = endDate) } + } + + private fun save() { + if (!currentState.isEnabled) return + } +} From ceeb84d0d92eb5da5162dc2aedee7428f6cda736 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:10:23 +0900 Subject: [PATCH 16/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20GoalEditorSideEffect?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/goal_editor/GoalEditorSideEffect.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt new file mode 100644 index 00000000..911b63fa --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt @@ -0,0 +1,8 @@ +package com.twix.goal_editor + +import com.twix.designsystem.components.toast.model.ToastType +import com.twix.ui.base.SideEffect + +interface GoalEditorSideEffect : SideEffect { + data class ShowToast(val message: String, val type: ToastType) : GoalEditorSideEffect +} From a98a03e1850ccc1cd4f11fa0d13209c11738dcbc Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:13:11 +0900 Subject: [PATCH 17/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20GoalEditorScreen=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20UI=20=EA=B5=AC=EC=A1=B0=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 --- .../com/twix/goal_editor/GoalEditorScreen.kt | 401 ++++++++++++++++++ 1 file changed, 401 insertions(+) create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt new file mode 100644 index 00000000..6a1c378a --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt @@ -0,0 +1,401 @@ +package com.twix.goal_editor + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +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.lifecycle.compose.collectAsStateWithLifecycle +import com.twix.designsystem.R +import com.twix.designsystem.components.button.AppButton +import com.twix.designsystem.components.common.CommonSwitch +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.components.text_field.UnderlineTextField +import com.twix.designsystem.components.toast.ToastManager +import com.twix.designsystem.components.toast.model.ToastData +import com.twix.designsystem.theme.CommonColor +import com.twix.designsystem.theme.GrayColor +import com.twix.designsystem.theme.TwixTheme +import com.twix.domain.model.enums.AppTextStyle +import com.twix.domain.model.enums.RepeatType +import com.twix.goal_editor.component.EmojiPicker +import com.twix.goal_editor.component.GoalEditorTopBar +import com.twix.goal_editor.model.GoalEditorUiState +import com.twix.ui.extension.dismissKeyboardOnTap +import com.twix.ui.extension.noRippleClickable +import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject +import org.koin.java.KoinJavaComponent.inject +import java.time.LocalDate + +@Composable +fun GoalEditorRoute( + viewModel: GoalEditorViewModel = koinViewModel(), + toastManager: ToastManager = koinInject(), + isEdit: Boolean = false, + navigateToBack: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.sideEffect.collect { effect -> + when(effect) { + is GoalEditorSideEffect.ShowToast -> toastManager.tryShow(ToastData(effect.message, effect.type)) + } + } + } + + GoalEditorScreen( + uiState = uiState, + isEdit = isEdit, + onBack = navigateToBack, + onCommitTitle = { viewModel.dispatch(GoalEditorIntent.UpdateTitle(it)) }, + onSelectedRepeatType = { viewModel.dispatch(GoalEditorIntent.UpdateRepeatType(it)) }, + ) +} + +@Composable +fun GoalEditorScreen( + uiState: GoalEditorUiState, + isEdit: Boolean = false, + onBack: () -> Unit, + onCommitTitle: (String) -> Unit, + onSelectedRepeatType: (RepeatType) -> Unit, +) { + Column( + modifier = + Modifier + .fillMaxSize() + .background(CommonColor.White) + .dismissKeyboardOnTap(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GoalEditorTopBar( + isEdit = isEdit, + onBack = onBack, + ) + + Spacer(Modifier.height(52.dp)) + + EmojiPicker( + emojiId = uiState.selectedIconId, + onClick = {}, + ) + + Spacer(Modifier.height(44.dp)) + + GoalTextField( + value = uiState.goalTitle, + onCommitTitle = onCommitTitle, + ) + + Spacer(Modifier.height(44.dp)) + + GoalInfoSection( + selectedRepeatType = uiState.selectedRepeatType, + repeatCount = uiState.repeatCount, + startDate = uiState.startDate, + endDate = uiState.endDate, + onSelectedRepeatType = onSelectedRepeatType, + onShowRepeatCountBottomSheet = {}, + onShowCalendarBottomSheet = {}, + ) + + Spacer(Modifier.weight(1f)) + + AppButton( + modifier = + Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth(), + text = stringResource(R.string.word_completion), + ) + } +} + +@Composable +private fun GoalTextField( + value: String, + onCommitTitle: (String) -> Unit, +) { + var internalValue by rememberSaveable(value) { mutableStateOf(value) } + // 초기에 무의미하게 commit 되는 것을 방지하는 상태 변수 + var wasFocused by remember { mutableStateOf(false) } + + UnderlineTextField( + modifier = + Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth() + .onFocusChanged { state -> + if (wasFocused && !state.isFocused) { + onCommitTitle(internalValue.trim()) + } + wasFocused = state.isFocused + }, + value = internalValue, + placeHolder = stringResource(R.string.goal_editor_text_field_placeholder), + onValueChange = { internalValue = it }, + ) +} + +@Composable +private fun GoalInfoSection( + selectedRepeatType: RepeatType, + repeatCount: Int, + startDate: LocalDate, + endDate: LocalDate?, + onSelectedRepeatType: (RepeatType) -> Unit, + onShowRepeatCountBottomSheet: () -> Unit, + onShowCalendarBottomSheet: (Boolean) -> Unit, // true면 endDate +) { + var endDateVisible by remember { mutableStateOf(false) } + + Column( + modifier = + Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth() + .border(1.dp, GrayColor.C500, RoundedCornerShape(12.dp)), + ) { + RepeatTypeSection( + selectedRepeatType = selectedRepeatType, + repeatCount = repeatCount, + onSelectedRepeatType = onSelectedRepeatType, + onShowRepeatCountBottomSheet = onShowRepeatCountBottomSheet, + ) + + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + + DateSection( + date = startDate, + onShowCalendarBottomSheet = { onShowCalendarBottomSheet(false) }, + ) + + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + + EndDateSwitchSection( + visible = endDateVisible, + onToggle = { endDateVisible = it }, + ) + + AnimatedVisibility( + visible = endDateVisible, + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + ) { + Column { + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + + DateSection( + date = endDate ?: LocalDate.now(), + isEndDate = true, + onShowCalendarBottomSheet = { onShowCalendarBottomSheet(true) }, + ) + } + } + } +} + +@Composable +private fun RepeatTypeSection( + selectedRepeatType: RepeatType, + repeatCount: Int, + onSelectedRepeatType: (RepeatType) -> Unit, + onShowRepeatCountBottomSheet: () -> Unit, +) { + val animationDuration = 160 + + Column( + modifier = + Modifier + .padding(16.dp), + ) { + HeaderText(stringResource(R.string.header_repeat_type)) + + Spacer(Modifier.height(12.dp)) + + Row( + modifier = + Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + RepeatType.entries.forEachIndexed { index, type -> + val isSelected = selectedRepeatType == type + + AppText( + text = type.label(), + style = AppTextStyle.B2, + color = if (isSelected) CommonColor.White else GrayColor.C500, + modifier = + Modifier + .clip(RoundedCornerShape(8.dp)) + .background(if (isSelected) GrayColor.C500 else CommonColor.White) + .border(1.dp, GrayColor.C500, RoundedCornerShape(8.dp)) + .padding(horizontal = 12.dp, vertical = 5.5.dp) + .noRippleClickable(onClick = { onSelectedRepeatType(type) }), + ) + + if (index != RepeatType.entries.lastIndex) Spacer(Modifier.width(8.dp)) + } + + Spacer(Modifier.weight(1f)) + + AnimatedVisibility( + visible = selectedRepeatType != RepeatType.DAILY, + enter = fadeIn(animationSpec = tween(durationMillis = animationDuration)), + exit = fadeOut(animationSpec = tween(durationMillis = animationDuration)), + ) { + Row( + modifier = + Modifier + .noRippleClickable(onClick = onShowRepeatCountBottomSheet), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AppText( + text = "%s %s번".format(selectedRepeatType.label(), repeatCount), + style = AppTextStyle.B2, + color = GrayColor.C500, + ) + + Image( + painter = painterResource(R.drawable.ic_chevron_down_circle), + contentDescription = "repeat type", + modifier = + Modifier + .size(24.dp), + ) + } + } + } + } +} + +@Composable +private fun DateSection( + date: LocalDate, + isEndDate: Boolean = false, + onShowCalendarBottomSheet: () -> Unit, +) { + Row( + modifier = + Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + HeaderText(stringResource(if (isEndDate) R.string.header_end_date else R.string.header_start_date)) + + Spacer(Modifier.weight(1f)) + + Row( + modifier = + Modifier + .noRippleClickable(onClick = onShowCalendarBottomSheet), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AppText( + text = "%s월 %s일".format(date.monthValue, date.dayOfMonth), + style = AppTextStyle.B2, + color = GrayColor.C500, + ) + + Image( + painter = painterResource(R.drawable.ic_chevron_down_circle), + contentDescription = "date", + modifier = + Modifier + .size(24.dp), + ) + } + } +} + +@Composable +private fun EndDateSwitchSection( + visible: Boolean = false, + onToggle: (Boolean) -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + HeaderText(stringResource(R.string.header_end_date_option)) + + Spacer(Modifier.weight(1f)) + + CommonSwitch( + checked = visible, + onClick = onToggle, + ) + } +} + +@Composable +private fun HeaderText(text: String) { + AppText( + text = text, + style = AppTextStyle.B1, + color = GrayColor.C500, + ) +} + +@Composable +private fun RepeatType.label(): String = + when (this) { + RepeatType.DAILY -> stringResource(R.string.word_daily) + RepeatType.WEEKLY -> stringResource(R.string.word_weekly) + RepeatType.MONTHLY -> stringResource(R.string.word_monthly) + } + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TwixTheme { + val uiState = GoalEditorUiState() + + GoalEditorScreen( + uiState = uiState, + onBack = {}, + onCommitTitle = {}, + onSelectedRepeatType = {}, + ) + } +} From 74a16aa2d3768790172fa8f58982cfe3abe6f425 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 28 Jan 2026 23:13:33 +0900 Subject: [PATCH 18/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20HomeScreen=20->=20Goa?= =?UTF-8?q?lEditorScreen=20=EB=84=A4=EB=B9=84=EA=B2=8C=EC=9D=B4=EC=85=98?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/twix/navigation/NavRoutes.kt | 7 +++++++ feature/main/src/main/java/com/twix/home/HomeScreen.kt | 7 ++++--- feature/main/src/main/java/com/twix/main/MainScreen.kt | 8 +++++++- .../main/java/com/twix/main/navigation/MainNavGraph.kt | 8 +++++++- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt index 625bc78e..687aeab0 100644 --- a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt +++ b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt @@ -23,4 +23,11 @@ sealed class NavRoutes( object MainGraph : NavRoutes("main_graph") object MainRoute : NavRoutes("main") + + /** + * GoalEditorGraph + * */ + object GoalEditorGraph : NavRoutes("goal_editor_graph") + + object GoalEditorRoute : NavRoutes("goal_editor") } diff --git a/feature/main/src/main/java/com/twix/home/HomeScreen.kt b/feature/main/src/main/java/com/twix/home/HomeScreen.kt index 31a02b65..d2e453c3 100644 --- a/feature/main/src/main/java/com/twix/home/HomeScreen.kt +++ b/feature/main/src/main/java/com/twix/home/HomeScreen.kt @@ -33,6 +33,7 @@ import java.time.LocalDate fun HomeRoute( viewModel: HomeViewModel = koinViewModel(), onShowCalendarBottomSheet: () -> Unit, + navigateToGoalEditor: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -44,6 +45,7 @@ fun HomeRoute( onUpdateVisibleDate = { viewModel.dispatch(HomeIntent.UpdateVisibleDate(it)) }, onMoveToToday = { viewModel.dispatch(HomeIntent.MoveToToday) }, onShowCalendarBottomSheet = onShowCalendarBottomSheet, + onAddNewGoal = navigateToGoalEditor, ) } @@ -56,6 +58,7 @@ fun HomeScreen( onUpdateVisibleDate: (LocalDate) -> Unit, onMoveToToday: () -> Unit, onShowCalendarBottomSheet: () -> Unit, + onAddNewGoal: () -> Unit, ) { Box( modifier = @@ -94,9 +97,7 @@ fun HomeScreen( Modifier .align(Alignment.BottomEnd) .padding(bottom = 12.dp, end = 16.dp), - onClick = { - // TODO: 목표 추가 화면으로 이동 - }, + onClick = onAddNewGoal, ) } } diff --git a/feature/main/src/main/java/com/twix/main/MainScreen.kt b/feature/main/src/main/java/com/twix/main/MainScreen.kt index ac51fd2c..1e367709 100644 --- a/feature/main/src/main/java/com/twix/main/MainScreen.kt +++ b/feature/main/src/main/java/com/twix/main/MainScreen.kt @@ -24,7 +24,10 @@ import com.twix.main.model.MainTab import org.koin.androidx.compose.koinViewModel @Composable -fun MainRoute(viewModel: MainViewModel = koinViewModel()) { +fun MainRoute( + viewModel: MainViewModel = koinViewModel(), + navigateToGoalEditor: () -> Unit, +) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val homeViewModel: HomeViewModel = koinViewModel() @@ -32,6 +35,7 @@ fun MainRoute(viewModel: MainViewModel = koinViewModel()) { homeViewModel = homeViewModel, selectedTab = uiState.selectedTab, onTabClick = { tab -> viewModel.dispatch(MainIntent.SelectTab(tab)) }, + navigateToGoalEditor = navigateToGoalEditor, ) } @@ -40,6 +44,7 @@ private fun MainScreen( homeViewModel: HomeViewModel, selectedTab: MainTab, onTabClick: (MainTab) -> Unit, + navigateToGoalEditor: () -> Unit, ) { val calendarState by homeViewModel.calendarState.collectAsStateWithLifecycle() var showCalendarBottomSheet by remember { mutableStateOf(false) } @@ -73,6 +78,7 @@ private fun MainScreen( HomeRoute( viewModel = homeViewModel, onShowCalendarBottomSheet = { showCalendarBottomSheet = true }, + navigateToGoalEditor = navigateToGoalEditor, ) MainTab.STATS -> Box(modifier = Modifier.fillMaxSize()) MainTab.COUPLE -> Box(modifier = Modifier.fillMaxSize()) diff --git a/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt b/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt index a96a4e1f..a5456e1e 100644 --- a/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt +++ b/feature/main/src/main/java/com/twix/main/navigation/MainNavGraph.kt @@ -20,7 +20,13 @@ object MainNavGraph : NavGraphContributor { startDestination = startDestination, ) { composable(NavRoutes.MainRoute.route) { - MainRoute() + MainRoute( + navigateToGoalEditor = { + navController.navigate(NavRoutes.GoalEditorRoute.route) { + launchSingleTop = true + } + }, + ) } } } From 14c1a5fe95878312313ba312d60a860bc16e2c1d Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 29 Jan 2026 23:33:45 +0900 Subject: [PATCH 19/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/twix/goal_editor/GoalEditorScreen.kt | 276 +----------------- .../goal_editor/component/GoalInfoCard.kt | 257 ++++++++++++++++ .../goal_editor/component/GoalTextField.kt | 42 +++ 3 files changed, 303 insertions(+), 272 deletions(-) create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt create mode 100644 feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt index 6a1c378a..6ed65184 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt @@ -1,63 +1,36 @@ package com.twix.goal_editor -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border -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.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider 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.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.onFocusChanged -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.lifecycle.compose.collectAsStateWithLifecycle import com.twix.designsystem.R import com.twix.designsystem.components.button.AppButton -import com.twix.designsystem.components.common.CommonSwitch -import com.twix.designsystem.components.text.AppText -import com.twix.designsystem.components.text_field.UnderlineTextField import com.twix.designsystem.components.toast.ToastManager import com.twix.designsystem.components.toast.model.ToastData import com.twix.designsystem.theme.CommonColor -import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme -import com.twix.domain.model.enums.AppTextStyle import com.twix.domain.model.enums.RepeatType import com.twix.goal_editor.component.EmojiPicker import com.twix.goal_editor.component.GoalEditorTopBar +import com.twix.goal_editor.component.GoalInfoCard +import com.twix.goal_editor.component.GoalTextField import com.twix.goal_editor.model.GoalEditorUiState import com.twix.ui.extension.dismissKeyboardOnTap -import com.twix.ui.extension.noRippleClickable import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject -import org.koin.java.KoinJavaComponent.inject -import java.time.LocalDate @Composable fun GoalEditorRoute( @@ -70,7 +43,7 @@ fun GoalEditorRoute( LaunchedEffect(Unit) { viewModel.sideEffect.collect { effect -> - when(effect) { + when (effect) { is GoalEditorSideEffect.ShowToast -> toastManager.tryShow(ToastData(effect.message, effect.type)) } } @@ -122,7 +95,7 @@ fun GoalEditorScreen( Spacer(Modifier.height(44.dp)) - GoalInfoSection( + GoalInfoCard( selectedRepeatType = uiState.selectedRepeatType, repeatCount = uiState.repeatCount, startDate = uiState.startDate, @@ -144,247 +117,6 @@ fun GoalEditorScreen( } } -@Composable -private fun GoalTextField( - value: String, - onCommitTitle: (String) -> Unit, -) { - var internalValue by rememberSaveable(value) { mutableStateOf(value) } - // 초기에 무의미하게 commit 되는 것을 방지하는 상태 변수 - var wasFocused by remember { mutableStateOf(false) } - - UnderlineTextField( - modifier = - Modifier - .padding(horizontal = 20.dp) - .fillMaxWidth() - .onFocusChanged { state -> - if (wasFocused && !state.isFocused) { - onCommitTitle(internalValue.trim()) - } - wasFocused = state.isFocused - }, - value = internalValue, - placeHolder = stringResource(R.string.goal_editor_text_field_placeholder), - onValueChange = { internalValue = it }, - ) -} - -@Composable -private fun GoalInfoSection( - selectedRepeatType: RepeatType, - repeatCount: Int, - startDate: LocalDate, - endDate: LocalDate?, - onSelectedRepeatType: (RepeatType) -> Unit, - onShowRepeatCountBottomSheet: () -> Unit, - onShowCalendarBottomSheet: (Boolean) -> Unit, // true면 endDate -) { - var endDateVisible by remember { mutableStateOf(false) } - - Column( - modifier = - Modifier - .padding(horizontal = 20.dp) - .fillMaxWidth() - .border(1.dp, GrayColor.C500, RoundedCornerShape(12.dp)), - ) { - RepeatTypeSection( - selectedRepeatType = selectedRepeatType, - repeatCount = repeatCount, - onSelectedRepeatType = onSelectedRepeatType, - onShowRepeatCountBottomSheet = onShowRepeatCountBottomSheet, - ) - - HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) - - DateSection( - date = startDate, - onShowCalendarBottomSheet = { onShowCalendarBottomSheet(false) }, - ) - - HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) - - EndDateSwitchSection( - visible = endDateVisible, - onToggle = { endDateVisible = it }, - ) - - AnimatedVisibility( - visible = endDateVisible, - enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), - exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), - ) { - Column { - HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) - - DateSection( - date = endDate ?: LocalDate.now(), - isEndDate = true, - onShowCalendarBottomSheet = { onShowCalendarBottomSheet(true) }, - ) - } - } - } -} - -@Composable -private fun RepeatTypeSection( - selectedRepeatType: RepeatType, - repeatCount: Int, - onSelectedRepeatType: (RepeatType) -> Unit, - onShowRepeatCountBottomSheet: () -> Unit, -) { - val animationDuration = 160 - - Column( - modifier = - Modifier - .padding(16.dp), - ) { - HeaderText(stringResource(R.string.header_repeat_type)) - - Spacer(Modifier.height(12.dp)) - - Row( - modifier = - Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - RepeatType.entries.forEachIndexed { index, type -> - val isSelected = selectedRepeatType == type - - AppText( - text = type.label(), - style = AppTextStyle.B2, - color = if (isSelected) CommonColor.White else GrayColor.C500, - modifier = - Modifier - .clip(RoundedCornerShape(8.dp)) - .background(if (isSelected) GrayColor.C500 else CommonColor.White) - .border(1.dp, GrayColor.C500, RoundedCornerShape(8.dp)) - .padding(horizontal = 12.dp, vertical = 5.5.dp) - .noRippleClickable(onClick = { onSelectedRepeatType(type) }), - ) - - if (index != RepeatType.entries.lastIndex) Spacer(Modifier.width(8.dp)) - } - - Spacer(Modifier.weight(1f)) - - AnimatedVisibility( - visible = selectedRepeatType != RepeatType.DAILY, - enter = fadeIn(animationSpec = tween(durationMillis = animationDuration)), - exit = fadeOut(animationSpec = tween(durationMillis = animationDuration)), - ) { - Row( - modifier = - Modifier - .noRippleClickable(onClick = onShowRepeatCountBottomSheet), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - AppText( - text = "%s %s번".format(selectedRepeatType.label(), repeatCount), - style = AppTextStyle.B2, - color = GrayColor.C500, - ) - - Image( - painter = painterResource(R.drawable.ic_chevron_down_circle), - contentDescription = "repeat type", - modifier = - Modifier - .size(24.dp), - ) - } - } - } - } -} - -@Composable -private fun DateSection( - date: LocalDate, - isEndDate: Boolean = false, - onShowCalendarBottomSheet: () -> Unit, -) { - Row( - modifier = - Modifier - .padding(16.dp) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - HeaderText(stringResource(if (isEndDate) R.string.header_end_date else R.string.header_start_date)) - - Spacer(Modifier.weight(1f)) - - Row( - modifier = - Modifier - .noRippleClickable(onClick = onShowCalendarBottomSheet), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - AppText( - text = "%s월 %s일".format(date.monthValue, date.dayOfMonth), - style = AppTextStyle.B2, - color = GrayColor.C500, - ) - - Image( - painter = painterResource(R.drawable.ic_chevron_down_circle), - contentDescription = "date", - modifier = - Modifier - .size(24.dp), - ) - } - } -} - -@Composable -private fun EndDateSwitchSection( - visible: Boolean = false, - onToggle: (Boolean) -> Unit, -) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - HeaderText(stringResource(R.string.header_end_date_option)) - - Spacer(Modifier.weight(1f)) - - CommonSwitch( - checked = visible, - onClick = onToggle, - ) - } -} - -@Composable -private fun HeaderText(text: String) { - AppText( - text = text, - style = AppTextStyle.B1, - color = GrayColor.C500, - ) -} - -@Composable -private fun RepeatType.label(): String = - when (this) { - RepeatType.DAILY -> stringResource(R.string.word_daily) - RepeatType.WEEKLY -> stringResource(R.string.word_weekly) - RepeatType.MONTHLY -> stringResource(R.string.word_monthly) - } - @Preview(showBackground = true) @Composable private fun Preview() { diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt new file mode 100644 index 00000000..818fa2ae --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt @@ -0,0 +1,257 @@ +package com.twix.goal_editor.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.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.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.common.CommonSwitch +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.theme.CommonColor +import com.twix.designsystem.theme.GrayColor +import com.twix.domain.model.enums.AppTextStyle +import com.twix.domain.model.enums.RepeatType +import com.twix.ui.extension.noRippleClickable +import java.time.LocalDate + +@Composable +fun GoalInfoCard( + selectedRepeatType: RepeatType, + repeatCount: Int, + startDate: LocalDate, + endDate: LocalDate, + onSelectedRepeatType: (RepeatType) -> Unit, + onShowRepeatCountBottomSheet: () -> Unit, + onShowCalendarBottomSheet: (Boolean) -> Unit, // true면 endDate +) { + var endDateVisible by remember { mutableStateOf(false) } + + Column( + modifier = + Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth() + .border(1.dp, GrayColor.C500, RoundedCornerShape(12.dp)), + ) { + RepeatTypeSettings( + selectedRepeatType = selectedRepeatType, + repeatCount = repeatCount, + onSelectedRepeatType = onSelectedRepeatType, + onShowRepeatCountBottomSheet = onShowRepeatCountBottomSheet, + ) + + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + + DateSettings( + date = startDate, + onShowCalendarBottomSheet = { onShowCalendarBottomSheet(false) }, + ) + + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + + EndDateOption( + visible = endDateVisible, + onToggle = { endDateVisible = it }, + ) + + AnimatedVisibility( + visible = endDateVisible, + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + ) { + Column { + HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) + + DateSettings( + date = endDate, + isEndDate = true, + onShowCalendarBottomSheet = { onShowCalendarBottomSheet(true) }, + ) + } + } + } +} + +@Composable +private fun RepeatTypeSettings( + selectedRepeatType: RepeatType, + repeatCount: Int, + onSelectedRepeatType: (RepeatType) -> Unit, + onShowRepeatCountBottomSheet: () -> Unit, +) { + val animationDuration = 160 + + Column( + modifier = + Modifier + .padding(16.dp), + ) { + HeaderText(stringResource(R.string.header_repeat_type)) + + Spacer(Modifier.height(12.dp)) + + Row( + modifier = + Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + RepeatType.entries.forEachIndexed { index, type -> + val isSelected = selectedRepeatType == type + + AppText( + text = type.label(), + style = AppTextStyle.B2, + color = if (isSelected) CommonColor.White else GrayColor.C500, + modifier = + Modifier + .clip(RoundedCornerShape(8.dp)) + .background(if (isSelected) GrayColor.C500 else CommonColor.White) + .border(1.dp, GrayColor.C500, RoundedCornerShape(8.dp)) + .padding(horizontal = 12.dp, vertical = 5.5.dp) + .noRippleClickable(onClick = { onSelectedRepeatType(type) }), + ) + + if (index != RepeatType.entries.lastIndex) Spacer(Modifier.width(8.dp)) + } + + Spacer(Modifier.weight(1f)) + + AnimatedVisibility( + visible = selectedRepeatType != RepeatType.DAILY, + enter = fadeIn(animationSpec = tween(durationMillis = animationDuration)), + exit = fadeOut(animationSpec = tween(durationMillis = animationDuration)), + ) { + Row( + modifier = + Modifier + .noRippleClickable(onClick = onShowRepeatCountBottomSheet), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AppText( + text = "%s %s번".format(selectedRepeatType.label(), repeatCount), + style = AppTextStyle.B2, + color = GrayColor.C500, + ) + + Image( + painter = painterResource(R.drawable.ic_chevron_down_circle), + contentDescription = "repeat type", + modifier = + Modifier + .size(24.dp), + ) + } + } + } + } +} + +@Composable +private fun DateSettings( + date: LocalDate, + isEndDate: Boolean = false, + onShowCalendarBottomSheet: () -> Unit, +) { + Row( + modifier = + Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + HeaderText(stringResource(if (isEndDate) R.string.header_end_date else R.string.header_start_date)) + + Spacer(Modifier.weight(1f)) + + Row( + modifier = + Modifier + .noRippleClickable(onClick = onShowCalendarBottomSheet), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AppText( + text = "%s월 %s일".format(date.monthValue, date.dayOfMonth), + style = AppTextStyle.B2, + color = GrayColor.C500, + ) + + Image( + painter = painterResource(R.drawable.ic_chevron_down_circle), + contentDescription = "date", + modifier = + Modifier + .size(24.dp), + ) + } + } +} + +@Composable +private fun EndDateOption( + visible: Boolean = false, + onToggle: (Boolean) -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + HeaderText(stringResource(R.string.header_end_date_option)) + + Spacer(Modifier.weight(1f)) + + CommonSwitch( + checked = visible, + onClick = onToggle, + ) + } +} + +@Composable +private fun HeaderText(text: String) { + AppText( + text = text, + style = AppTextStyle.B1, + color = GrayColor.C500, + ) +} + +@Composable +private fun RepeatType.label(): String = + when (this) { + RepeatType.DAILY -> stringResource(R.string.word_daily) + RepeatType.WEEKLY -> stringResource(R.string.word_weekly) + RepeatType.MONTHLY -> stringResource(R.string.word_monthly) + } diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt new file mode 100644 index 00000000..eedfedbc --- /dev/null +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt @@ -0,0 +1,42 @@ +package com.twix.goal_editor.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.text_field.UnderlineTextField + +@Composable +fun GoalTextField( + value: String, + onCommitTitle: (String) -> Unit, +) { + var internalValue by rememberSaveable(value) { mutableStateOf(value) } + // 초기에 무의미하게 commit 되는 것을 방지하는 상태 변수 + var wasFocused by remember { mutableStateOf(false) } + + UnderlineTextField( + modifier = + Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth() + .onFocusChanged { state -> + if (wasFocused && !state.isFocused) { + onCommitTitle(internalValue.trim()) + } + wasFocused = state.isFocused + }, + value = internalValue, + placeHolder = stringResource(R.string.goal_editor_text_field_placeholder), + onValueChange = { internalValue = it }, + ) +} From f714475024406826514172ddb03ea252beadf7b8 Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 29 Jan 2026 23:34:02 +0900 Subject: [PATCH 20/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20nullable?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/twix/goal_editor/model/GoalEditorUiState.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt index 1ba03685..6e02ad8b 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt @@ -12,7 +12,7 @@ data class GoalEditorUiState( val selectedRepeatType: RepeatType = RepeatType.DAILY, val repeatCount: Int = 0, val startDate: LocalDate = LocalDate.now(), - val endDate: LocalDate? = null, + val endDate: LocalDate = LocalDate.now(), ) : State { val isEnabled: Boolean get() = selectedIconId != -1L && goalTitle.isNotBlank() && repeatCount > 0 From b2763345f18a5683ac8de909829b67b7a9fe6665 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 30 Jan 2026 01:32:29 +0900 Subject: [PATCH 21/43] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20=EB=A6=AC?= =?UTF-8?q?=EC=86=8C=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 --- .../design-system/src/main/res/drawable/ic_minus.xml | 12 ++++++++++++ core/design-system/src/main/res/drawable/ic_plus.xml | 12 ++++++------ core/design-system/src/main/res/values/strings.xml | 1 + 3 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 core/design-system/src/main/res/drawable/ic_minus.xml diff --git a/core/design-system/src/main/res/drawable/ic_minus.xml b/core/design-system/src/main/res/drawable/ic_minus.xml new file mode 100644 index 00000000..508d5507 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/design-system/src/main/res/drawable/ic_plus.xml b/core/design-system/src/main/res/drawable/ic_plus.xml index 1785a79e..c9db1c8a 100644 --- a/core/design-system/src/main/res/drawable/ic_plus.xml +++ b/core/design-system/src/main/res/drawable/ic_plus.xml @@ -1,12 +1,12 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index cf234fa9..34337574 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ 커플페이지 오늘 완료 + 매일 매주 From 723377bf59dba60382a23a0e05eb9b07881b4534 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 30 Jan 2026 01:32:48 +0900 Subject: [PATCH 22/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20ColorFil?= =?UTF-8?q?ter=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/main/src/main/java/com/twix/home/HomeScreen.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/feature/main/src/main/java/com/twix/home/HomeScreen.kt b/feature/main/src/main/java/com/twix/home/HomeScreen.kt index d2e453c3..422138df 100644 --- a/feature/main/src/main/java/com/twix/home/HomeScreen.kt +++ b/feature/main/src/main/java/com/twix/home/HomeScreen.kt @@ -16,10 +16,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.twix.designsystem.R +import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor import com.twix.home.component.EmptyGoalGuide import com.twix.home.component.HomeTopBar @@ -122,6 +124,7 @@ private fun AddGoalButton( modifier = Modifier .size(40.dp), + colorFilter = ColorFilter.tint(CommonColor.White), ) } } From c20590ade8adf7ec040639a60ac6956f6ca9b190 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 30 Jan 2026 01:33:24 +0900 Subject: [PATCH 23/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20Intent?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/twix/goal_editor/GoalEditorIntent.kt | 16 +++++--- .../twix/goal_editor/GoalEditorViewModel.kt | 39 +++++++++++++------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt index ec8206ef..a6326318 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt @@ -5,29 +5,33 @@ import com.twix.ui.base.Intent import java.time.LocalDate sealed interface GoalEditorIntent : Intent { - data class SelectIcon( + data class SetIcon( val iconId: Long, ) : GoalEditorIntent - data class UpdateTitle( + data class SetTitle( val title: String, ) : GoalEditorIntent - data class UpdateRepeatType( + data class SetRepeatType( val repeatType: RepeatType, ) : GoalEditorIntent - data class UpdateRepeatCount( + data class SetRepeatCount( val repeatCount: Int, ) : GoalEditorIntent - data class UpdateStartDate( + data class SetStartDate( val startDate: LocalDate, ) : GoalEditorIntent - data class UpdateEndDate( + data class SetEndDate( val endDate: LocalDate, ) : GoalEditorIntent + data class SetEndDateEnabled( + val enabled: Boolean, + ) : GoalEditorIntent + data object Save : GoalEditorIntent } diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt index a88a29ac..f07b40df 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt @@ -1,8 +1,11 @@ package com.twix.goal_editor +import androidx.lifecycle.viewModelScope +import com.twix.designsystem.components.toast.model.ToastType import com.twix.domain.model.enums.RepeatType import com.twix.goal_editor.model.GoalEditorUiState import com.twix.ui.base.BaseViewModel +import kotlinx.coroutines.launch import java.time.LocalDate class GoalEditorViewModel : @@ -12,46 +15,58 @@ class GoalEditorViewModel : override suspend fun handleIntent(intent: GoalEditorIntent) { when (intent) { GoalEditorIntent.Save -> save() - is GoalEditorIntent.SelectIcon -> selectIcon(intent.iconId) - is GoalEditorIntent.UpdateEndDate -> updateEndDate(intent.endDate) - is GoalEditorIntent.UpdateRepeatCount -> updateRepeatCount(intent.repeatCount) - is GoalEditorIntent.UpdateRepeatType -> updateRepeatType(intent.repeatType) - is GoalEditorIntent.UpdateStartDate -> updateStartDate(intent.startDate) - is GoalEditorIntent.UpdateTitle -> updateTitle(intent.title) + is GoalEditorIntent.SetIcon -> setIcon(intent.iconId) + is GoalEditorIntent.SetEndDate -> setEndDate(intent.endDate) + is GoalEditorIntent.SetRepeatCount -> setRepeatCount(intent.repeatCount) + is GoalEditorIntent.SetRepeatType -> setRepeatType(intent.repeatType) + is GoalEditorIntent.SetStartDate -> setStartDate(intent.startDate) + is GoalEditorIntent.SetTitle -> setTitle(intent.title) + is GoalEditorIntent.SetEndDateEnabled -> setEndDateEnabled(intent.enabled) } } - private fun selectIcon(iconId: Long) { + private fun setIcon(iconId: Long) { if (iconId <= 0) return reduce { copy(selectedIconId = iconId) } } - private fun updateTitle(title: String) { + private fun setTitle(title: String) { if (title.isBlank()) return reduce { copy(goalTitle = title) } } - private fun updateRepeatType(repeatType: RepeatType) { + private fun setRepeatType(repeatType: RepeatType) { reduce { copy(selectedRepeatType = repeatType) } } - private fun updateRepeatCount(repeatCount: Int) { + private fun setRepeatCount(repeatCount: Int) { if (repeatCount <= 0) return reduce { copy(repeatCount = repeatCount) } } - private fun updateStartDate(startDate: LocalDate) { + private fun setStartDate(startDate: LocalDate) { reduce { copy(startDate = startDate) } } - private fun updateEndDate(endDate: LocalDate) { + private fun setEndDate(endDate: LocalDate) { reduce { copy(endDate = endDate) } } + private fun setEndDateEnabled(enabled: Boolean) { + reduce { copy(endDateEnabled = enabled) } + } + private fun save() { if (!currentState.isEnabled) return + + if (currentState.endDateEnabled && currentState.endDate.isBefore(currentState.startDate)) { + viewModelScope.launch { + emitSideEffect(GoalEditorSideEffect.ShowToast("종료 날짜가 시작 날짜보다 이전입니다.", ToastType.ERROR)) + } + return + } } } From c20fc1ca805c2a9113a961d79ee4291c7d6c29b5 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 30 Jan 2026 01:34:12 +0900 Subject: [PATCH 24/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20endDateE?= =?UTF-8?q?nabled=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../twix/goal_editor/component/GoalInfoCard.kt | 16 ++++++---------- .../twix/goal_editor/model/GoalEditorUiState.kt | 1 + 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt index 818fa2ae..07418036 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt @@ -21,10 +21,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -46,13 +42,13 @@ fun GoalInfoCard( selectedRepeatType: RepeatType, repeatCount: Int, startDate: LocalDate, + endDateEnabled: Boolean, endDate: LocalDate, onSelectedRepeatType: (RepeatType) -> Unit, onShowRepeatCountBottomSheet: () -> Unit, onShowCalendarBottomSheet: (Boolean) -> Unit, // true면 endDate + onToggleEndDateEnabled: (Boolean) -> Unit, ) { - var endDateVisible by remember { mutableStateOf(false) } - Column( modifier = Modifier @@ -77,12 +73,12 @@ fun GoalInfoCard( HorizontalDivider(thickness = 1.dp, color = GrayColor.C500) EndDateOption( - visible = endDateVisible, - onToggle = { endDateVisible = it }, + visible = endDateEnabled, + onToggle = onToggleEndDateEnabled, ) AnimatedVisibility( - visible = endDateVisible, + visible = endDateEnabled, enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), ) { @@ -249,7 +245,7 @@ private fun HeaderText(text: String) { } @Composable -private fun RepeatType.label(): String = +fun RepeatType.label(): String = when (this) { RepeatType.DAILY -> stringResource(R.string.word_daily) RepeatType.WEEKLY -> stringResource(R.string.word_weekly) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt index 6e02ad8b..37a4b780 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt @@ -12,6 +12,7 @@ data class GoalEditorUiState( val selectedRepeatType: RepeatType = RepeatType.DAILY, val repeatCount: Int = 0, val startDate: LocalDate = LocalDate.now(), + val endDateEnabled: Boolean = false, val endDate: LocalDate = LocalDate.now(), ) : State { val isEnabled: Boolean From c09fd4781f2b962ac27c3691d49d5587e03109f2 Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 30 Jan 2026 01:34:51 +0900 Subject: [PATCH 25/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=B0=94=ED=85=80?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EB=B0=8F=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=EB=A1=9C=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 --- .../com/twix/goal_editor/GoalEditorScreen.kt | 283 +++++++++++++++--- 1 file changed, 248 insertions(+), 35 deletions(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt index 6ed65184..a20b2a5c 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt @@ -1,36 +1,61 @@ package com.twix.goal_editor +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +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.lifecycle.compose.collectAsStateWithLifecycle import com.twix.designsystem.R +import com.twix.designsystem.components.bottomsheet.CommonBottomSheet +import com.twix.designsystem.components.bottomsheet.model.CommonBottomSheetConfig import com.twix.designsystem.components.button.AppButton +import com.twix.designsystem.components.calendar.Calendar +import com.twix.designsystem.components.text.AppText import com.twix.designsystem.components.toast.ToastManager import com.twix.designsystem.components.toast.model.ToastData import com.twix.designsystem.theme.CommonColor +import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme +import com.twix.domain.model.enums.AppTextStyle import com.twix.domain.model.enums.RepeatType import com.twix.goal_editor.component.EmojiPicker import com.twix.goal_editor.component.GoalEditorTopBar import com.twix.goal_editor.component.GoalInfoCard import com.twix.goal_editor.component.GoalTextField +import com.twix.goal_editor.component.label import com.twix.goal_editor.model.GoalEditorUiState import com.twix.ui.extension.dismissKeyboardOnTap +import com.twix.ui.extension.noRippleClickable import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject +import java.time.LocalDate @Composable fun GoalEditorRoute( @@ -53,8 +78,13 @@ fun GoalEditorRoute( uiState = uiState, isEdit = isEdit, onBack = navigateToBack, - onCommitTitle = { viewModel.dispatch(GoalEditorIntent.UpdateTitle(it)) }, - onSelectedRepeatType = { viewModel.dispatch(GoalEditorIntent.UpdateRepeatType(it)) }, + onCommitTitle = { viewModel.dispatch(GoalEditorIntent.SetTitle(it)) }, + onSelectRepeatType = { viewModel.dispatch(GoalEditorIntent.SetRepeatType(it)) }, + onCommitEndDate = { viewModel.dispatch(GoalEditorIntent.SetEndDate(it)) }, + onCommitStartDate = { viewModel.dispatch(GoalEditorIntent.SetStartDate(it)) }, + onCommitRepeatCount = { viewModel.dispatch(GoalEditorIntent.SetRepeatCount(it)) }, + onToggleEndDateEnabled = { viewModel.dispatch(GoalEditorIntent.SetEndDateEnabled(it)) }, + onComplete = { viewModel.dispatch(GoalEditorIntent.Save) }, ) } @@ -64,53 +94,230 @@ fun GoalEditorScreen( isEdit: Boolean = false, onBack: () -> Unit, onCommitTitle: (String) -> Unit, - onSelectedRepeatType: (RepeatType) -> Unit, + onSelectRepeatType: (RepeatType) -> Unit, + onCommitEndDate: (LocalDate) -> Unit, + onCommitStartDate: (LocalDate) -> Unit, + onCommitRepeatCount: (Int) -> Unit, + onToggleEndDateEnabled: (Boolean) -> Unit, + onComplete: () -> Unit, ) { + var showRepeatCountBottomSheet by remember { mutableStateOf(false) } + var showCalendarBottomSheet by remember { mutableStateOf(false) } + var isEndDate by remember { mutableStateOf(true) } + + Box { + Column( + modifier = + Modifier + .fillMaxSize() + .background(CommonColor.White) + .dismissKeyboardOnTap(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + GoalEditorTopBar( + isEdit = isEdit, + onBack = onBack, + ) + + Spacer(Modifier.height(52.dp)) + + EmojiPicker( + emojiId = uiState.selectedIconId, + onClick = {}, + ) + + Spacer(Modifier.height(44.dp)) + + GoalTextField( + value = uiState.goalTitle, + onCommitTitle = onCommitTitle, + ) + + Spacer(Modifier.height(44.dp)) + + GoalInfoCard( + selectedRepeatType = uiState.selectedRepeatType, + repeatCount = uiState.repeatCount, + startDate = uiState.startDate, + endDateEnabled = uiState.endDateEnabled, + endDate = uiState.endDate, + onSelectedRepeatType = onSelectRepeatType, + onShowRepeatCountBottomSheet = { showRepeatCountBottomSheet = true }, + onShowCalendarBottomSheet = { + isEndDate = it + showCalendarBottomSheet = true + }, + onToggleEndDateEnabled = onToggleEndDateEnabled, + ) + + Spacer(Modifier.weight(1f)) + + AppButton( + onClick = onComplete, + modifier = + Modifier + .padding(horizontal = 20.dp) + .padding(vertical = 8.dp) + .fillMaxWidth(), + text = stringResource(R.string.word_completion), + ) + } + + CommonBottomSheet( + visible = showCalendarBottomSheet, + config = CommonBottomSheetConfig(showHandle = false), + onDismissRequest = { showCalendarBottomSheet = false }, + content = { + Calendar( + initialDate = if (isEndDate) uiState.endDate else uiState.startDate, + onComplete = { + if (isEndDate) onCommitEndDate(it) else onCommitStartDate(it) + showCalendarBottomSheet = false + }, + ) + }, + ) + + CommonBottomSheet( + visible = showRepeatCountBottomSheet, + config = CommonBottomSheetConfig(showHandle = false), + onDismissRequest = { showRepeatCountBottomSheet = false }, + content = { + RepeatCountBottomSheetContent( + repeatCount = uiState.repeatCount, + selectedRepeatType = uiState.selectedRepeatType, + onCommit = { repeatType, repeatCount -> + onSelectRepeatType(repeatType) + onCommitRepeatCount(repeatCount) + showRepeatCountBottomSheet = false + }, + ) + }, + ) + } +} + +@Composable +private fun RepeatCountBottomSheetContent( + repeatCount: Int, + selectedRepeatType: RepeatType, + onCommit: (RepeatType, Int) -> Unit, +) { + var internalRepeatCount by remember { mutableIntStateOf(repeatCount) } + var internalSelectedRepeatType by remember { mutableStateOf(selectedRepeatType) } + val maxCount = if (internalSelectedRepeatType == RepeatType.WEEKLY) 6 else 25 + Column( - modifier = - Modifier - .fillMaxSize() - .background(CommonColor.White) - .dismissKeyboardOnTap(), horizontalAlignment = Alignment.CenterHorizontally, ) { - GoalEditorTopBar( - isEdit = isEdit, - onBack = onBack, - ) + Spacer(Modifier.height(8.dp)) - Spacer(Modifier.height(52.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + AppText( + text = RepeatType.WEEKLY.label(), + style = AppTextStyle.B2, + color = if (internalSelectedRepeatType == RepeatType.WEEKLY) CommonColor.White else GrayColor.C500, + modifier = + Modifier + .clip(RoundedCornerShape(8.dp)) + .background(if (internalSelectedRepeatType == RepeatType.WEEKLY) GrayColor.C500 else CommonColor.White) + .border(1.dp, GrayColor.C500, RoundedCornerShape(8.dp)) + .padding(horizontal = 12.dp, vertical = 5.5.dp) + .noRippleClickable(onClick = { + internalSelectedRepeatType = RepeatType.WEEKLY + internalRepeatCount = 0 + }), + ) - EmojiPicker( - emojiId = uiState.selectedIconId, - onClick = {}, - ) + AppText( + text = RepeatType.MONTHLY.label(), + style = AppTextStyle.B2, + color = if (internalSelectedRepeatType == RepeatType.MONTHLY) CommonColor.White else GrayColor.C500, + modifier = + Modifier + .clip(RoundedCornerShape(8.dp)) + .background(if (internalSelectedRepeatType == RepeatType.MONTHLY) GrayColor.C500 else CommonColor.White) + .border(1.dp, GrayColor.C500, RoundedCornerShape(8.dp)) + .padding(horizontal = 12.dp, vertical = 5.5.dp) + .noRippleClickable(onClick = { + internalSelectedRepeatType = RepeatType.MONTHLY + internalRepeatCount = 0 + }), + ) + } - Spacer(Modifier.height(44.dp)) + Spacer(Modifier.height(36.dp)) - GoalTextField( - value = uiState.goalTitle, - onCommitTitle = onCommitTitle, - ) + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(R.drawable.ic_minus), + contentDescription = "minus", + colorFilter = ColorFilter.tint(CommonColor.White), + modifier = + Modifier + .background(GrayColor.C500, CircleShape) + .padding(4.dp) + .size(28.dp) + .noRippleClickable(onClick = { if (internalRepeatCount > 1) internalRepeatCount-- }), + ) - Spacer(Modifier.height(44.dp)) + Row( + modifier = + Modifier + .width(96.dp) + .border(1.dp, GrayColor.C300, RoundedCornerShape(12.dp)) + .padding(horizontal = 27.5.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + AppText( + text = internalRepeatCount.toString(), + style = AppTextStyle.H2, + color = GrayColor.C500, + ) - GoalInfoCard( - selectedRepeatType = uiState.selectedRepeatType, - repeatCount = uiState.repeatCount, - startDate = uiState.startDate, - endDate = uiState.endDate, - onSelectedRepeatType = onSelectedRepeatType, - onShowRepeatCountBottomSheet = {}, - onShowCalendarBottomSheet = {}, - ) + Spacer(Modifier.width(8.dp)) + + Box( + modifier = + Modifier + .size(24.dp), + contentAlignment = Alignment.Center, + ) { + AppText( + text = stringResource(R.string.word_count), + style = AppTextStyle.T2, + color = GrayColor.C300, + ) + } + } + + Image( + painter = painterResource(R.drawable.ic_plus), + contentDescription = "plus", + colorFilter = ColorFilter.tint(CommonColor.White), + modifier = + Modifier + .background(GrayColor.C500, CircleShape) + .padding(4.dp) + .size(28.dp) + .noRippleClickable(onClick = { if (maxCount > internalRepeatCount) internalRepeatCount++ }), + ) + } - Spacer(Modifier.weight(1f)) + Spacer(Modifier.height(32.dp)) AppButton( + onClick = { onCommit(internalSelectedRepeatType, internalRepeatCount) }, modifier = Modifier - .padding(horizontal = 20.dp) + .padding(horizontal = 20.dp, vertical = 8.dp) .fillMaxWidth(), text = stringResource(R.string.word_completion), ) @@ -127,7 +334,13 @@ private fun Preview() { uiState = uiState, onBack = {}, onCommitTitle = {}, - onSelectedRepeatType = {}, + onSelectRepeatType = {}, + onCommitEndDate = {}, + onCommitStartDate = {}, + onCommitRepeatCount = {}, + onToggleEndDateEnabled = {}, + onComplete = {}, + isEdit = false, ) } } From fa6f6928cb29750b3b9b635ce223ab0350cdd4ec Mon Sep 17 00:00:00 2001 From: dogmania Date: Fri, 30 Jan 2026 01:35:06 +0900 Subject: [PATCH 26/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20GoalEditorSideEffect?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/twix/goal_editor/GoalEditorSideEffect.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt index 911b63fa..99a0b366 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt @@ -4,5 +4,8 @@ import com.twix.designsystem.components.toast.model.ToastType import com.twix.ui.base.SideEffect interface GoalEditorSideEffect : SideEffect { - data class ShowToast(val message: String, val type: ToastType) : GoalEditorSideEffect + data class ShowToast( + val message: String, + val type: ToastType, + ) : GoalEditorSideEffect } From ece6582bb8377af9a06211883b0d63070e8edb27 Mon Sep 17 00:00:00 2001 From: dogmania Date: Sun, 1 Feb 2026 00:35:25 +0900 Subject: [PATCH 27/43] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=A6=AC=EC=86=8C=EC=8A=A4=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 --- .../src/main/res/drawable/ic_book.xml | 61 +++++++++ .../src/main/res/drawable/ic_clean.xml | 95 ++++++++++++++ .../src/main/res/drawable/ic_default.xml | 67 ++++++++++ .../src/main/res/drawable/ic_empty_emoji.xml | 117 ++++++++++++++++++ .../src/main/res/drawable/ic_exercise.xml | 31 +++++ .../src/main/res/drawable/ic_health.xml | 68 ++++++++++ .../src/main/res/drawable/ic_heart.xml | 20 +++ .../src/main/res/drawable/ic_laptop.xml | 113 +++++++++++++++++ .../src/main/res/drawable/ic_pencil.xml | 76 ++++++++++++ 9 files changed, 648 insertions(+) create mode 100644 core/design-system/src/main/res/drawable/ic_book.xml create mode 100644 core/design-system/src/main/res/drawable/ic_clean.xml create mode 100644 core/design-system/src/main/res/drawable/ic_default.xml create mode 100644 core/design-system/src/main/res/drawable/ic_empty_emoji.xml create mode 100644 core/design-system/src/main/res/drawable/ic_exercise.xml create mode 100644 core/design-system/src/main/res/drawable/ic_health.xml create mode 100644 core/design-system/src/main/res/drawable/ic_heart.xml create mode 100644 core/design-system/src/main/res/drawable/ic_laptop.xml create mode 100644 core/design-system/src/main/res/drawable/ic_pencil.xml diff --git a/core/design-system/src/main/res/drawable/ic_book.xml b/core/design-system/src/main/res/drawable/ic_book.xml new file mode 100644 index 00000000..a074b48c --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_book.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_clean.xml b/core/design-system/src/main/res/drawable/ic_clean.xml new file mode 100644 index 00000000..8b55c2f3 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_clean.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_default.xml b/core/design-system/src/main/res/drawable/ic_default.xml new file mode 100644 index 00000000..a3f2737c --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_default.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_empty_emoji.xml b/core/design-system/src/main/res/drawable/ic_empty_emoji.xml new file mode 100644 index 00000000..8fe6191c --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_empty_emoji.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_exercise.xml b/core/design-system/src/main/res/drawable/ic_exercise.xml new file mode 100644 index 00000000..d39953cb --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_exercise.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_health.xml b/core/design-system/src/main/res/drawable/ic_health.xml new file mode 100644 index 00000000..7442af1f --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_health.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_heart.xml b/core/design-system/src/main/res/drawable/ic_heart.xml new file mode 100644 index 00000000..250e3a5d --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,20 @@ + + + + diff --git a/core/design-system/src/main/res/drawable/ic_laptop.xml b/core/design-system/src/main/res/drawable/ic_laptop.xml new file mode 100644 index 00000000..33daae37 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_laptop.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_pencil.xml b/core/design-system/src/main/res/drawable/ic_pencil.xml new file mode 100644 index 00000000..b9df9fb6 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_pencil.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + From 049f96fccbf4a612d1251a0fef901e8708a1b516 Mon Sep 17 00:00:00 2001 From: dogmania Date: Sun, 1 Feb 2026 00:35:38 +0900 Subject: [PATCH 28/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20CommonDialog=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 --- .../components/dialog/CommonDialog.kt | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 core/design-system/src/main/java/com/twix/designsystem/components/dialog/CommonDialog.kt diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/dialog/CommonDialog.kt b/core/design-system/src/main/java/com/twix/designsystem/components/dialog/CommonDialog.kt new file mode 100644 index 00000000..83b98483 --- /dev/null +++ b/core/design-system/src/main/java/com/twix/designsystem/components/dialog/CommonDialog.kt @@ -0,0 +1,187 @@ +package com.twix.designsystem.components.dialog + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.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.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twix.designsystem.components.button.AppButton +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.theme.CommonColor +import com.twix.designsystem.theme.DimmedColor +import com.twix.designsystem.theme.GrayColor +import com.twix.designsystem.theme.TwixTheme +import com.twix.domain.model.enums.AppTextStyle +import com.twix.ui.extension.noRippleClickable + +@Composable +fun CommonDialog( + modifier: Modifier = Modifier, + visible: Boolean, + confirmText: String, + dismissText: String? = null, + onDismissRequest: () -> Unit, + content: @Composable ColumnScope.() -> Unit, + onConfirm: () -> Unit, + onDismiss: (() -> Unit)? = null, +) { + BackHandler { onDismissRequest() } + + Box( + modifier = + Modifier + .fillMaxSize() + .then(modifier), + contentAlignment = Alignment.Center, + ) { + DialogScrim(visible = visible, onDismissRequest = onDismissRequest) + + DialogContent( + modifier = + Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth(), + visible = visible, + confirmText = confirmText, + dismissText = dismissText, + content = content, + onConfirm = onConfirm, + onDismiss = onDismiss, + ) + } +} + +@Composable +private fun DialogScrim( + visible: Boolean, + onDismissRequest: () -> Unit, +) { + val fadeDuration = 160 + + AnimatedVisibility( + visible = visible, + enter = fadeIn(animationSpec = tween(fadeDuration)), + exit = fadeOut(animationSpec = tween(fadeDuration)), + modifier = Modifier.fillMaxSize(), + ) { + Box( + modifier = + Modifier + .fillMaxSize() + .background(DimmedColor.D070) + .noRippleClickable(onClick = onDismissRequest), + ) + } +} + +@Composable +private fun DialogContent( + modifier: Modifier, + visible: Boolean, + confirmText: String, + dismissText: String? = null, + content: @Composable ColumnScope.() -> Unit, + onConfirm: () -> Unit, + onDismiss: (() -> Unit)? = null, +) { + val fadeDuration = 160 + + AnimatedVisibility( + visible = visible, + enter = fadeIn(animationSpec = tween(fadeDuration)), + exit = fadeOut(animationSpec = tween(fadeDuration)), + ) { + Surface( + shape = RoundedCornerShape(20.dp), + color = CommonColor.White, + modifier = modifier, + ) { + Column( + modifier = + Modifier + .padding(horizontal = 20.dp) + .padding(top = 24.dp, bottom = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + content() + + Spacer(Modifier.height(24.dp)) + + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) { + if (dismissText != null && onDismiss != null) { + AppButton( + modifier = Modifier.weight(1f), + text = dismissText, + textColor = GrayColor.C500, + backgroundColor = CommonColor.White, + border = BorderStroke(1.dp, GrayColor.C500), + onClick = onDismiss, + ) + + Spacer(Modifier.width(8.dp)) + } + + AppButton( + modifier = Modifier.weight(1f), + text = confirmText, + onClick = onConfirm, + ) + } + } + } + } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun Preview() { + TwixTheme { + CommonDialog( + visible = true, + confirmText = "확인", + dismissText = "취소", + onDismissRequest = {}, + onConfirm = {}, + onDismiss = {}, + content = { + AppText( + text = "타이틀 텍스트", + style = AppTextStyle.T1, + color = GrayColor.C500, + ) + + Spacer(Modifier.height(8.dp)) + + AppText( + text = "내용 텍스트", + style = AppTextStyle.B2, + color = GrayColor.C400, + ) + }, + ) + } +} From 32c0ba0488ac9ca16f5064bdec7adfa7ac35695d Mon Sep 17 00:00:00 2001 From: dogmania Date: Sun, 1 Feb 2026 00:35:53 +0900 Subject: [PATCH 29/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20GoalIconType=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/domain/model/enums/GoalIconType.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 domain/src/main/java/com/twix/domain/model/enums/GoalIconType.kt diff --git a/domain/src/main/java/com/twix/domain/model/enums/GoalIconType.kt b/domain/src/main/java/com/twix/domain/model/enums/GoalIconType.kt new file mode 100644 index 00000000..79abb906 --- /dev/null +++ b/domain/src/main/java/com/twix/domain/model/enums/GoalIconType.kt @@ -0,0 +1,12 @@ +package com.twix.domain.model.enums + +enum class GoalIconType { + DEFAULT, + CLEAN, + EXERCISE, + BOOK, + PENCIL, + HEALTH, + HEART, + LAPTOP, +} From 16c5bb085b0c0975a5fb8b157e65db8a8aea4216 Mon Sep 17 00:00:00 2001 From: dogmania Date: Sun, 1 Feb 2026 00:36:12 +0900 Subject: [PATCH 30/43] =?UTF-8?q?=F0=9F=8D=B1=20Chore:=20string=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=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 --- core/design-system/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 34337574..bf6932f0 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -32,5 +32,6 @@ 시작 날짜 종료 날짜 종료 날짜 설정 + 아이콘 변경 \ No newline at end of file From f081bf7f5a85f1214c8663dfd5612d38c2dffd37 Mon Sep 17 00:00:00 2001 From: dogmania Date: Sun, 1 Feb 2026 00:36:53 +0900 Subject: [PATCH 31/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20GoalIconType=20->=20R?= =?UTF-8?q?es=20Id=20=EB=B3=80=ED=99=98=20=ED=99=95=EC=9E=A5=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../twix/goal_editor/component/EmojiPicker.kt | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/EmojiPicker.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/EmojiPicker.kt index 69931ba5..d3aa81c8 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/EmojiPicker.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/EmojiPicker.kt @@ -14,11 +14,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.twix.designsystem.R import com.twix.designsystem.theme.GrayColor +import com.twix.domain.model.enums.GoalIconType import com.twix.ui.extension.noRippleClickable @Composable fun EmojiPicker( - emojiId: Long, + icon: GoalIconType, onClick: () -> Unit, ) { Box( @@ -31,12 +32,7 @@ fun EmojiPicker( .noRippleClickable(onClick = onClick), ) { Image( - painter = - if (emojiId == -1L) { - painterResource(R.drawable.ic_emoji_add) - } else { - painterResource(R.drawable.ic_emoji_add) - }, + painter = painterResource(icon.toRes()), contentDescription = "emoji", modifier = Modifier @@ -44,3 +40,16 @@ fun EmojiPicker( ) } } + +@Composable +fun GoalIconType.toRes(): Int = + when (this) { + GoalIconType.DEFAULT -> R.drawable.ic_default + GoalIconType.CLEAN -> R.drawable.ic_clean + GoalIconType.EXERCISE -> R.drawable.ic_exercise + GoalIconType.BOOK -> R.drawable.ic_book + GoalIconType.PENCIL -> R.drawable.ic_pencil + GoalIconType.HEALTH -> R.drawable.ic_health + GoalIconType.HEART -> R.drawable.ic_heart + GoalIconType.LAPTOP -> R.drawable.ic_laptop + } From da82dbb4dd2875c637736c0d3387a91e22014b89 Mon Sep 17 00:00:00 2001 From: dogmania Date: Sun, 1 Feb 2026 00:37:11 +0900 Subject: [PATCH 32/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20=ED=82=A4=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=99=84=EB=A3=8C=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C?= =?UTF-8?q?=20=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/goal_editor/component/GoalTextField.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt index eedfedbc..48669b98 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalTextField.kt @@ -2,6 +2,8 @@ package com.twix.goal_editor.component import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -10,7 +12,9 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import com.twix.designsystem.R import com.twix.designsystem.components.text_field.UnderlineTextField @@ -20,6 +24,7 @@ fun GoalTextField( value: String, onCommitTitle: (String) -> Unit, ) { + val focusManager = LocalFocusManager.current var internalValue by rememberSaveable(value) { mutableStateOf(value) } // 초기에 무의미하게 commit 되는 것을 방지하는 상태 변수 var wasFocused by remember { mutableStateOf(false) } @@ -38,5 +43,7 @@ fun GoalTextField( value = internalValue, placeHolder = stringResource(R.string.goal_editor_text_field_placeholder), onValueChange = { internalValue = it }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), ) } From d5019347905c61c9a3e784275044073dd30f9a48 Mon Sep 17 00:00:00 2001 From: dogmania Date: Sun, 1 Feb 2026 00:37:39 +0900 Subject: [PATCH 33/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20selected?= =?UTF-8?q?IconId=20->=20selectedIcon=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/goal_editor/model/GoalEditorUiState.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt index 37a4b780..feee3572 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/model/GoalEditorUiState.kt @@ -1,13 +1,14 @@ package com.twix.goal_editor.model import androidx.compose.runtime.Immutable +import com.twix.domain.model.enums.GoalIconType import com.twix.domain.model.enums.RepeatType import com.twix.ui.base.State import java.time.LocalDate @Immutable data class GoalEditorUiState( - val selectedIconId: Long = -1L, + val selectedIcon: GoalIconType = GoalIconType.DEFAULT, val goalTitle: String = "", val selectedRepeatType: RepeatType = RepeatType.DAILY, val repeatCount: Int = 0, @@ -16,5 +17,5 @@ data class GoalEditorUiState( val endDate: LocalDate = LocalDate.now(), ) : State { val isEnabled: Boolean - get() = selectedIconId != -1L && goalTitle.isNotBlank() && repeatCount > 0 + get() = goalTitle.isNotBlank() && repeatCount > 0 } From f054f5b44af8a17486e99130c8e9eb50ce52a125 Mon Sep 17 00:00:00 2001 From: dogmania Date: Sun, 1 Feb 2026 00:38:08 +0900 Subject: [PATCH 34/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20selected?= =?UTF-8?q?IconId=20->=20selectedIcon=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 --- .../java/com/twix/goal_editor/GoalEditorViewModel.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt index f07b40df..0faf6a5e 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt @@ -2,6 +2,7 @@ package com.twix.goal_editor import androidx.lifecycle.viewModelScope import com.twix.designsystem.components.toast.model.ToastType +import com.twix.domain.model.enums.GoalIconType import com.twix.domain.model.enums.RepeatType import com.twix.goal_editor.model.GoalEditorUiState import com.twix.ui.base.BaseViewModel @@ -15,7 +16,7 @@ class GoalEditorViewModel : override suspend fun handleIntent(intent: GoalEditorIntent) { when (intent) { GoalEditorIntent.Save -> save() - is GoalEditorIntent.SetIcon -> setIcon(intent.iconId) + is GoalEditorIntent.SetIcon -> setIcon(intent.icon) is GoalEditorIntent.SetEndDate -> setEndDate(intent.endDate) is GoalEditorIntent.SetRepeatCount -> setRepeatCount(intent.repeatCount) is GoalEditorIntent.SetRepeatType -> setRepeatType(intent.repeatType) @@ -25,10 +26,8 @@ class GoalEditorViewModel : } } - private fun setIcon(iconId: Long) { - if (iconId <= 0) return - - reduce { copy(selectedIconId = iconId) } + private fun setIcon(icon: GoalIconType) { + reduce { copy(selectedIcon = icon) } } private fun setTitle(title: String) { From fac91eb04c35317f3c635821df21ce71c90755ef Mon Sep 17 00:00:00 2001 From: dogmania Date: Sun, 1 Feb 2026 00:38:31 +0900 Subject: [PATCH 35/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20selected?= =?UTF-8?q?IconId=20->=20selectedIcon=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 --- .../src/main/java/com/twix/goal_editor/GoalEditorIntent.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt index a6326318..0ac5656b 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorIntent.kt @@ -1,12 +1,13 @@ package com.twix.goal_editor +import com.twix.domain.model.enums.GoalIconType import com.twix.domain.model.enums.RepeatType import com.twix.ui.base.Intent import java.time.LocalDate sealed interface GoalEditorIntent : Intent { data class SetIcon( - val iconId: Long, + val icon: GoalIconType, ) : GoalEditorIntent data class SetTitle( From 56159c5c7091b7ca9e562d7679e3363c264334d9 Mon Sep 17 00:00:00 2001 From: dogmania Date: Sun, 1 Feb 2026 00:43:22 +0900 Subject: [PATCH 36/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=AA=A9=ED=91=9C=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=84=A0=ED=83=9D=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EB=A1=9C=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 --- .../com/twix/goal_editor/GoalEditorScreen.kt | 85 ++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt index a20b2a5c..730e98f4 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt @@ -14,6 +14,9 @@ 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.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -37,6 +40,7 @@ import com.twix.designsystem.components.bottomsheet.CommonBottomSheet import com.twix.designsystem.components.bottomsheet.model.CommonBottomSheetConfig import com.twix.designsystem.components.button.AppButton import com.twix.designsystem.components.calendar.Calendar +import com.twix.designsystem.components.dialog.CommonDialog import com.twix.designsystem.components.text.AppText import com.twix.designsystem.components.toast.ToastManager import com.twix.designsystem.components.toast.model.ToastData @@ -44,12 +48,14 @@ import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme import com.twix.domain.model.enums.AppTextStyle +import com.twix.domain.model.enums.GoalIconType import com.twix.domain.model.enums.RepeatType import com.twix.goal_editor.component.EmojiPicker import com.twix.goal_editor.component.GoalEditorTopBar import com.twix.goal_editor.component.GoalInfoCard import com.twix.goal_editor.component.GoalTextField import com.twix.goal_editor.component.label +import com.twix.goal_editor.component.toRes import com.twix.goal_editor.model.GoalEditorUiState import com.twix.ui.extension.dismissKeyboardOnTap import com.twix.ui.extension.noRippleClickable @@ -80,6 +86,7 @@ fun GoalEditorRoute( onBack = navigateToBack, onCommitTitle = { viewModel.dispatch(GoalEditorIntent.SetTitle(it)) }, onSelectRepeatType = { viewModel.dispatch(GoalEditorIntent.SetRepeatType(it)) }, + onCommitIcon = { viewModel.dispatch(GoalEditorIntent.SetIcon(it)) }, onCommitEndDate = { viewModel.dispatch(GoalEditorIntent.SetEndDate(it)) }, onCommitStartDate = { viewModel.dispatch(GoalEditorIntent.SetStartDate(it)) }, onCommitRepeatCount = { viewModel.dispatch(GoalEditorIntent.SetRepeatCount(it)) }, @@ -95,6 +102,7 @@ fun GoalEditorScreen( onBack: () -> Unit, onCommitTitle: (String) -> Unit, onSelectRepeatType: (RepeatType) -> Unit, + onCommitIcon: (GoalIconType) -> Unit, onCommitEndDate: (LocalDate) -> Unit, onCommitStartDate: (LocalDate) -> Unit, onCommitRepeatCount: (Int) -> Unit, @@ -103,7 +111,9 @@ fun GoalEditorScreen( ) { var showRepeatCountBottomSheet by remember { mutableStateOf(false) } var showCalendarBottomSheet by remember { mutableStateOf(false) } + var showIconEditorDialog by remember { mutableStateOf(false) } var isEndDate by remember { mutableStateOf(true) } + var internalSelectedIcon by remember { mutableStateOf(uiState.selectedIcon) } Box { Column( @@ -122,8 +132,8 @@ fun GoalEditorScreen( Spacer(Modifier.height(52.dp)) EmojiPicker( - emojiId = uiState.selectedIconId, - onClick = {}, + icon = uiState.selectedIcon, + onClick = { showIconEditorDialog = true }, ) Spacer(Modifier.height(44.dp)) @@ -194,6 +204,76 @@ fun GoalEditorScreen( ) }, ) + + CommonDialog( + visible = showIconEditorDialog, + confirmText = stringResource(R.string.word_completion), + onDismissRequest = { showIconEditorDialog = false }, + onConfirm = { + onCommitIcon(internalSelectedIcon) + showIconEditorDialog = false + }, + content = { + IconEditorDialogContent( + selectedIcon = internalSelectedIcon, + onClick = { internalSelectedIcon = it }, + ) + }, + ) + } +} + +@Composable +private fun IconEditorDialogContent( + selectedIcon: GoalIconType, + onClick: (GoalIconType) -> Unit, +) { + AppText( + text = stringResource(R.string.icon_editor_dialog_title), + style = AppTextStyle.T1, + color = GrayColor.C500, + ) + + Spacer(Modifier.height(16.dp)) + + LazyVerticalGrid( + columns = GridCells.Fixed(4), + horizontalArrangement = Arrangement.SpaceBetween, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(GoalIconType.entries) { + val isSelected = it == selectedIcon + + /** + * LazyVerticalGrid(GridCells.Fixed(4))는 각 아이템을 전체 폭을 4등분한 값으로 측정함 + * 따라서 아이템 루트에 CircleShape Border를 적용해도 LazyVerticalGrid가 측정한 아이템 폭에 의해 타원처럼 보일 수 있음 + * + * 그래서 바깥 Box가 LazyVerticalGrid가 측정한 폭을 받아 정렬만 담당하고, + * 실제 원형 영역은 안쪽 Box(size + CircleShape)로 적용함 + */ + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = + Modifier + .size(64.dp) + .clip(CircleShape) + .border(1.dp, if (isSelected) GrayColor.C500 else GrayColor.C100, CircleShape) + .noRippleClickable(onClick = { onClick(it) }), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(it.toRes()), + contentDescription = "emoji", + modifier = + Modifier + .size(42.dp), + ) + } + } + } } } @@ -340,6 +420,7 @@ private fun Preview() { onCommitRepeatCount = {}, onToggleEndDateEnabled = {}, onComplete = {}, + onCommitIcon = {}, isEdit = false, ) } From 1aedb5c315702412e3fd93d061a1b70eb5857f9f Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 4 Feb 2026 16:59:13 +0900 Subject: [PATCH 37/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0?= =?UTF-8?q?=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/twix/task_certification/component/CameraControlBar.kt | 4 ++-- .../com/twix/task_certification/component/CameraPreviewBox.kt | 2 +- .../task_certification/component/TaskCertificationTopBar.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraControlBar.kt b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraControlBar.kt index 02e66d83..7f302816 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraControlBar.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraControlBar.kt @@ -51,14 +51,14 @@ internal fun CameraControlBar( contentDescription = null, modifier = Modifier - .noRippleClickable(onCaptureClick), + .noRippleClickable(onClick = onCaptureClick), ) Image( imageVector = ImageVector.vectorResource(R.drawable.ic_camera_toggle), contentDescription = null, modifier = Modifier - .noRippleClickable(onToggleCameraClick), + .noRippleClickable(onClick = onToggleCameraClick), ) } } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt index 79486dc7..d6ae9356 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/component/CameraPreviewBox.kt @@ -97,7 +97,7 @@ private fun TorchIcon( contentDescription = null, modifier = Modifier - .noRippleClickable(onClickFlash) + .noRippleClickable(onClick = onClickFlash) .padding(start = 30.33.dp, top = 31.82.dp), ) } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/component/TaskCertificationTopBar.kt b/feature/task-certification/src/main/java/com/twix/task_certification/component/TaskCertificationTopBar.kt index 32c917c3..e070201c 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/component/TaskCertificationTopBar.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/component/TaskCertificationTopBar.kt @@ -34,7 +34,7 @@ internal fun TaskCertificationTopBar( Modifier .padding(24.dp) .align(Alignment.CenterEnd) - .noRippleClickable(onClickClose), + .noRippleClickable(onClick = onClickClose), ) } } From cc0f653c742aaae11ed8cd5c566fa07617856f06 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 4 Feb 2026 17:03:30 +0900 Subject: [PATCH 38/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EB=86=92=EC=9D=B4=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/components/text_field/UnderlineTextField.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt b/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt index 790de3ce..c43b7a27 100644 --- a/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt +++ b/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt @@ -1,5 +1,6 @@ package com.twix.designsystem.components.text_field +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -36,7 +37,11 @@ fun UnderlineTextField( val typo = LocalAppTypography.current Column( + modifier = + Modifier + .height(52.dp), horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center, ) { Spacer(Modifier.height(4.dp)) From 4a7cf9ec486f9a7b78cb4390f5b1d4c71f70f217 Mon Sep 17 00:00:00 2001 From: dogmania Date: Wed, 4 Feb 2026 17:40:03 +0900 Subject: [PATCH 39/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20BackHand?= =?UTF-8?q?ler=20enabled=20=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/components/bottomsheet/CommonBottomSheet.kt | 2 +- .../com/twix/designsystem/components/dialog/CommonDialog.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/bottomsheet/CommonBottomSheet.kt b/core/design-system/src/main/java/com/twix/designsystem/components/bottomsheet/CommonBottomSheet.kt index 1c185742..cdc380a5 100644 --- a/core/design-system/src/main/java/com/twix/designsystem/components/bottomsheet/CommonBottomSheet.kt +++ b/core/design-system/src/main/java/com/twix/designsystem/components/bottomsheet/CommonBottomSheet.kt @@ -77,7 +77,7 @@ fun CommonBottomSheet( if (!rendering) return - BackHandler { onDismissRequest() } + BackHandler(enabled = internalVisible) { onDismissRequest() } BoxWithConstraints( modifier = diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/dialog/CommonDialog.kt b/core/design-system/src/main/java/com/twix/designsystem/components/dialog/CommonDialog.kt index 83b98483..82bcee08 100644 --- a/core/design-system/src/main/java/com/twix/designsystem/components/dialog/CommonDialog.kt +++ b/core/design-system/src/main/java/com/twix/designsystem/components/dialog/CommonDialog.kt @@ -44,7 +44,7 @@ fun CommonDialog( onConfirm: () -> Unit, onDismiss: (() -> Unit)? = null, ) { - BackHandler { onDismissRequest() } + BackHandler(enabled = visible) { onDismissRequest() } Box( modifier = From 76f73b0e942a034784759156e1897bb43cf35cd4 Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 5 Feb 2026 17:42:54 +0900 Subject: [PATCH 40/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=ED=95=98=EB=93=9C=EC=BD=94=EB=94=A9=20stringResource=EB=A1=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/design-system/src/main/res/values/strings.xml | 1 + .../main/java/com/twix/goal_editor/component/GoalInfoCard.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index bf6932f0..ca6ee06c 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -33,5 +33,6 @@ 종료 날짜 종료 날짜 설정 아이콘 변경 + %s %s번 \ No newline at end of file diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt index 07418036..c6d95d47 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt @@ -153,7 +153,7 @@ private fun RepeatTypeSettings( verticalAlignment = Alignment.CenterVertically, ) { AppText( - text = "%s %s번".format(selectedRepeatType.label(), repeatCount), + text = stringResource(R.string.repeat_count, selectedRepeatType.label(), repeatCount), style = AppTextStyle.B2, color = GrayColor.C500, ) From 3834bb6cbb6f009960a92cbd8243e75b9382019e Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 5 Feb 2026 18:23:13 +0900 Subject: [PATCH 41/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EB=B7=B0=EB=AA=A8=EB=8D=B8=EC=97=90=EC=84=9C=20ResId=EB=A5=BC?= =?UTF-8?q?=20=EC=A0=84=EB=8B=AC=ED=95=98=EA=B3=A0=20UI=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EA=B0=92=EC=9D=84=20=EA=B0=80=EC=A0=B8=EC=98=A4=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 --- core/design-system/src/main/res/values/strings.xml | 3 +++ .../main/java/com/twix/goal_editor/GoalEditorScreen.kt | 10 +++++++++- .../java/com/twix/goal_editor/GoalEditorSideEffect.kt | 3 ++- .../java/com/twix/goal_editor/GoalEditorViewModel.kt | 3 ++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index ca6ee06c..657bea88 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -35,4 +35,7 @@ 아이콘 변경 %s %s번 + + 종료 날짜가 시작 날짜보다 이전입니다. + \ No newline at end of file diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt index 730e98f4..f31b2cb8 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt @@ -25,11 +25,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter +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 @@ -71,11 +73,17 @@ fun GoalEditorRoute( navigateToBack: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current + /** + * LaunchedEffect 내부에서는 stringResource를 사용할 수 없고, context.getString을 그대로 사용하면 경고 발생 + * LaunchedEffect가 예전 context를 들고 있지 않도록 막아주는 용도 + * */ + val currentContext by rememberUpdatedState(context) LaunchedEffect(Unit) { viewModel.sideEffect.collect { effect -> when (effect) { - is GoalEditorSideEffect.ShowToast -> toastManager.tryShow(ToastData(effect.message, effect.type)) + is GoalEditorSideEffect.ShowToast -> toastManager.tryShow(ToastData(currentContext.getString(effect.resId), effect.type)) } } } diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt index 99a0b366..6e2fcd2a 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorSideEffect.kt @@ -1,11 +1,12 @@ package com.twix.goal_editor +import androidx.annotation.StringRes import com.twix.designsystem.components.toast.model.ToastType import com.twix.ui.base.SideEffect interface GoalEditorSideEffect : SideEffect { data class ShowToast( - val message: String, + @param:StringRes val resId: Int, val type: ToastType, ) : GoalEditorSideEffect } diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt index 0faf6a5e..c99766e8 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorViewModel.kt @@ -1,6 +1,7 @@ package com.twix.goal_editor import androidx.lifecycle.viewModelScope +import com.twix.designsystem.R import com.twix.designsystem.components.toast.model.ToastType import com.twix.domain.model.enums.GoalIconType import com.twix.domain.model.enums.RepeatType @@ -63,7 +64,7 @@ class GoalEditorViewModel : if (currentState.endDateEnabled && currentState.endDate.isBefore(currentState.startDate)) { viewModelScope.launch { - emitSideEffect(GoalEditorSideEffect.ShowToast("종료 날짜가 시작 날짜보다 이전입니다.", ToastType.ERROR)) + emitSideEffect(GoalEditorSideEffect.ShowToast(R.string.toast_end_date_before_start_date, ToastType.ERROR)) } return } From 9cd1e8158c0b405cbafe02bfc51416f2dbebdb46 Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 5 Feb 2026 18:36:08 +0900 Subject: [PATCH 42/43] =?UTF-8?q?=E2=9C=A8=20Feat:=20trailingIcon=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 --- .../text_field/UnderlineTextField.kt | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt b/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt index c43b7a27..ec7e58cb 100644 --- a/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt +++ b/core/design-system/src/main/java/com/twix/designsystem/components/text_field/UnderlineTextField.kt @@ -3,7 +3,9 @@ package com.twix.designsystem.components.text_field 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 import androidx.compose.foundation.text.BasicTextField @@ -29,9 +31,11 @@ fun UnderlineTextField( enabled: Boolean = true, readOnly: Boolean = false, singleLine: Boolean = true, + showTrailing: Boolean = false, maxLines: Int = 1, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, + trailing: (@Composable () -> Unit)? = null, onValueChange: (String) -> Unit, ) { val typo = LocalAppTypography.current @@ -51,6 +55,8 @@ fun UnderlineTextField( .padding(horizontal = 8.dp, vertical = 10.dp), contentAlignment = Alignment.CenterStart, ) { + val shouldShowTrailing = trailing != null && showTrailing && value.isNotBlank() + if (value.isBlank()) { AppText( text = placeHolder, @@ -62,17 +68,35 @@ fun UnderlineTextField( ) } - BasicTextField( - value = value, - textStyle = textStyle.toTextStyle(typo).copy(color = GrayColor.C500), - onValueChange = onValueChange, - enabled = enabled, - readOnly = readOnly, - singleLine = singleLine, - maxLines = maxLines, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - ) + Row( + modifier = + Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + BasicTextField( + value = value, + textStyle = textStyle.toTextStyle(typo).copy(color = GrayColor.C500), + onValueChange = onValueChange, + enabled = enabled, + readOnly = readOnly, + singleLine = singleLine, + maxLines = maxLines, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + ) + + if (shouldShowTrailing) { + Box( + modifier = + Modifier + .padding(start = 10.dp), + contentAlignment = Alignment.Center, + ) { + trailing.invoke() + } + } + } } Spacer(Modifier.height(4.dp)) From c9e510a52b4c7e25fbf1812364e35f885f2295e0 Mon Sep 17 00:00:00 2001 From: dogmania Date: Thu, 5 Feb 2026 18:36:27 +0900 Subject: [PATCH 43/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20Lint=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 --- .../src/main/java/com/twix/goal_editor/GoalEditorScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt index f31b2cb8..e3ec4af9 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/GoalEditorScreen.kt @@ -74,6 +74,7 @@ fun GoalEditorRoute( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + /** * LaunchedEffect 내부에서는 stringResource를 사용할 수 없고, context.getString을 그대로 사용하면 경고 발생 * LaunchedEffect가 예전 context를 들고 있지 않도록 막아주는 용도