From fcce3c27487c23bdbb83e4893eced4f203d4912e Mon Sep 17 00:00:00 2001
From: soyeonLee <109227292+soyeonLee126@users.noreply.github.com>
Date: Tue, 18 Aug 2026 09:05:51 +0900
Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95/=EB=8B=89?=
=?UTF-8?q?=EB=84=A4=EC=9E=84=20=EB=B3=80=EA=B2=BD=20=ED=99=94=EB=A9=B4=20?=
=?UTF-8?q?=EC=8B=9C=EC=95=88=20=EB=B0=98=EC=98=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 설정 목록에 알림 설정 항목과 ON/OFF 배지 추가
- OS 알림 허용 여부를 읽어 표시하고, 탭하면 앱별 알림 설정으로 이동
- 화면이 다시 보일 때마다 다시 읽어 배지를 갱신
- 목록 항목 높이/여백과 화면 배경을 시안 값으로 정렬
- 상단 내비게이션 배경을 화면 배경색으로 명시
기본값이 기기 다크 모드를 따라가 라이트로 고정된 셸과 어긋났다
- 시스템 바 아이콘 색을 밝은 배경용으로 고정
같은 이유로 기기가 다크 모드면 흰 배경에 흰 아이콘이 겹쳤다
- 닉네임 변경 화면 안내 문구/입력 필드/버튼 여백을 시안에 맞춤
입력 필드는 모서리가 직각이고 높이가 52dp라 OutlinedTextField 대신
BasicTextField 로 다시 그렸다
---
.../com/gamss/android/app/MainActivity.kt | 10 +++-
.../designsystem/textfield/GamssTextField.kt | 43 +++++++++++----
feature/setting/build.gradle.kts | 1 +
.../setting/accountinfo/AccountInfoScreen.kt | 6 ++-
.../setting/component/SettingListItem.kt | 52 ++++++++++++++++++-
.../feature/setting/main/SettingScreen.kt | 52 ++++++++++++++++++-
.../nicknamechange/NicknameChangeScreen.kt | 17 +++---
.../setting/src/main/res/values/strings.xml | 4 ++
gradle/libs.versions.toml | 1 +
9 files changed, 164 insertions(+), 22 deletions(-)
diff --git a/app/src/main/kotlin/com/gamss/android/app/MainActivity.kt b/app/src/main/kotlin/com/gamss/android/app/MainActivity.kt
index 5b8c72db..a830070e 100644
--- a/app/src/main/kotlin/com/gamss/android/app/MainActivity.kt
+++ b/app/src/main/kotlin/com/gamss/android/app/MainActivity.kt
@@ -1,7 +1,9 @@
package com.gamss.android.app
+import android.graphics.Color
import android.os.Bundle
import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.gamss.android.app.navigation.GamssRootNavHost
@@ -13,7 +15,13 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- enableEdgeToEdge()
+ // 인자 없는 기본값은 시스템 바 아이콘 색을 기기 다크 모드에 맞춘다. 하지만 셸이 라이트로
+ // 고정돼 있어(MainScreen 참고) 기기가 다크면 흰 배경에 흰 아이콘이 겹친다. 다크 시안이
+ // 나와 셸 고정을 풀 때 이 스타일도 함께 되돌린다.
+ enableEdgeToEdge(
+ statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.BLACK),
+ navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.BLACK),
+ )
setContent {
GamssTheme {
GamssRootNavHost()
diff --git a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt
index 9a7faad3..3d540c18 100644
--- a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt
+++ b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt
@@ -1,20 +1,30 @@
package com.gamss.android.core.designsystem.textfield
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.OutlinedTextFieldDefaults
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import com.gamss.android.core.designsystem.theme.GamssTheme
/**
- * 라벨 텍스트 + [OutlinedTextField] 조합의 입력 필드.
+ * 라벨 텍스트 + 입력 필드 조합의 입력 필드.
* 라벨이 필드 위에 별도로 표시되는 형태
+ *
+ * 시안의 필드는 모서리가 직각이고 높이가 52dp라, 최소 높이 56dp에 자체 여백을 가진
+ * [androidx.compose.material3.OutlinedTextField] 대신 [BasicTextField]로 그린다.
*/
@Composable
fun GamssTextField(
@@ -31,19 +41,32 @@ fun GamssTextField(
color = GamssTheme.colors.gray950,
)
Spacer(modifier = Modifier.height(GamssTheme.spacing.spacing200))
- OutlinedTextField(
+ BasicTextField(
modifier = Modifier.fillMaxWidth(),
value = value,
onValueChange = onValueChange,
singleLine = singleLine,
- colors = OutlinedTextFieldDefaults.colors(
- focusedBorderColor = GamssTheme.colors.gray200,
- unfocusedBorderColor = GamssTheme.colors.gray200,
- ),
- )
+ textStyle = GamssTheme.typography.body3Medium.copy(color = GamssTheme.colors.gray950),
+ cursorBrush = SolidColor(GamssTheme.colors.gray950),
+ ) { innerTextField ->
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = FieldMinHeight)
+ .background(GamssTheme.colors.gray025)
+ .border(width = FieldBorderWidth, color = GamssTheme.colors.gray950)
+ .padding(horizontal = GamssTheme.spacing.spacing300),
+ contentAlignment = Alignment.CenterStart,
+ ) {
+ innerTextField()
+ }
+ }
}
}
+private val FieldMinHeight = 52.dp
+private val FieldBorderWidth = 1.dp
+
@Preview(name = "GamssTextField", showBackground = true)
@Composable
@Suppress("UnusedPrivateMember")
@@ -51,7 +74,7 @@ private fun GamssTextFieldPreview() {
GamssTheme {
GamssTextField(
modifier = Modifier.fillMaxWidth(),
- label = "닉네임 변경",
+ label = "변경할 닉네임을 입력해주세요.",
value = "송지연",
onValueChange = {},
)
diff --git a/feature/setting/build.gradle.kts b/feature/setting/build.gradle.kts
index 1f7ac65c..1fc9ad9b 100644
--- a/feature/setting/build.gradle.kts
+++ b/feature/setting/build.gradle.kts
@@ -10,5 +10,6 @@ dependencies {
implementation(projects.core.common)
implementation(projects.core.ui)
implementation(projects.domain)
+ implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.compose.material.icons.core)
}
\ No newline at end of file
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/accountinfo/AccountInfoScreen.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/accountinfo/AccountInfoScreen.kt
index 328a5762..cf17d224 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/accountinfo/AccountInfoScreen.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/accountinfo/AccountInfoScreen.kt
@@ -73,10 +73,12 @@ fun AccountInfoScreen(
Column(
modifier = modifier
.fillMaxSize()
- .background(GamssTheme.colors.gray025),
+ .background(GamssTheme.colors.background),
) {
GamssTopNavigation(
- modifier = Modifier.padding(bottom = GamssTheme.spacing.spacing150),
+ modifier = Modifier.padding(bottom = GamssTheme.spacing.spacing300),
+ // 기본값은 기기의 다크 모드를 보므로, 라이트로 고정된 앱 테마와 어긋나지 않게 직접 넘긴다.
+ backgroundColor = GamssTheme.colors.background,
title = stringResource(R.string.setting_list_user_account),
showLeftIcon = true,
onLeftIconClick = onBackClick,
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt
index 12cd4ebc..23aa808d 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt
@@ -1,18 +1,23 @@
package com.gamss.android.feature.setting.component
+import androidx.annotation.StringRes
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.gamss.android.core.designsystem.modifier.noRippleClickableIfNotNull
import com.gamss.android.core.designsystem.theme.GamssTheme
+import com.gamss.android.feature.setting.R
@Suppress("LongParameterList")
@Composable
@@ -21,6 +26,7 @@ fun SettingListItem(
contentColor: Color = GamssTheme.colors.gray950,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
+ badge: SettingListItemBadge? = null,
additionalInfo: String? = null,
contentPadding: PaddingValues = SettingListItemDefaults.ContentPadding,
) {
@@ -37,6 +43,13 @@ fun SettingListItem(
color = contentColor,
)
+ if (badge != null) {
+ BadgeChip(
+ modifier = Modifier.padding(start = GamssTheme.spacing.spacing050),
+ badge = badge,
+ )
+ }
+
Spacer(modifier = Modifier.weight(1f))
if (additionalInfo != null) {
@@ -49,6 +62,43 @@ fun SettingListItem(
}
}
+/**
+ * 제목 바로 옆에 붙는 상태 배지입니다. 알림 설정의 ON/OFF 표시에 사용합니다.
+ *
+ * 라벨과 색을 한곳에 묶어 두어 두 상태가 서로 어긋나지 않게 합니다.
+ */
+enum class SettingListItemBadge(@StringRes internal val labelRes: Int) {
+ On(R.string.setting_badge_on),
+ Off(R.string.setting_badge_off),
+}
+
+@Composable
+private fun BadgeChip(
+ badge: SettingListItemBadge,
+ modifier: Modifier = Modifier,
+) {
+ val containerColor = when (badge) {
+ SettingListItemBadge.On -> GamssTheme.colors.apricot
+ SettingListItemBadge.Off -> GamssTheme.colors.gray200
+ }
+ val contentColor = when (badge) {
+ SettingListItemBadge.On -> GamssTheme.colors.white
+ SettingListItemBadge.Off -> GamssTheme.colors.gray900
+ }
+
+ Text(
+ modifier = modifier
+ .background(color = containerColor, shape = CircleShape)
+ .padding(
+ horizontal = GamssTheme.spacing.spacing075,
+ vertical = GamssTheme.spacing.spacing025,
+ ),
+ text = stringResource(badge.labelRes),
+ style = GamssTheme.typography.caption3,
+ color = contentColor,
+ )
+}
+
object SettingListItemDefaults {
- val ContentPadding = PaddingValues(horizontal = 18.dp, vertical = 12.dp)
+ val ContentPadding = PaddingValues(horizontal = 18.dp, vertical = 8.dp)
}
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt
index 297a95e9..945fdfdc 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt
@@ -1,6 +1,9 @@
package com.gamss.android.feature.setting.main
+import android.content.ActivityNotFoundException
import android.content.Context
+import android.content.Intent
+import android.provider.Settings
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -10,14 +13,22 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.net.toUri
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LifecycleEventEffect
import com.gamss.android.core.designsystem.theme.GamssTheme
import com.gamss.android.core.designsystem.topnavigation.GamssTopNavigation
import com.gamss.android.feature.setting.R
import com.gamss.android.feature.setting.component.SettingListItem
+import com.gamss.android.feature.setting.component.SettingListItemBadge
@Composable
fun SettingScreen(
@@ -30,14 +41,22 @@ fun SettingScreen(
val context = LocalContext.current
val appVersion = remember(context) { context.appVersion() }
+ // 알림 허용 여부는 앱 밖(OS 설정)에서 바뀌므로, 화면이 다시 보일 때마다 읽어 배지를 갱신한다.
+ var notificationsEnabled by remember(context) { mutableStateOf(context.notificationsEnabled()) }
+ LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
+ notificationsEnabled = context.notificationsEnabled()
+ }
+
Column(
modifier = modifier
.fillMaxSize()
- .background(GamssTheme.colors.gray025)
+ .background(GamssTheme.colors.background)
.verticalScroll(rememberScrollState()),
) {
GamssTopNavigation(
- modifier = Modifier.padding(bottom = GamssTheme.spacing.spacing150),
+ modifier = Modifier.padding(bottom = GamssTheme.spacing.spacing300),
+ // 기본값은 기기의 다크 모드를 보므로, 라이트로 고정된 앱 테마와 어긋나지 않게 직접 넘긴다.
+ backgroundColor = GamssTheme.colors.background,
title = stringResource(R.string.setting_title),
showLeftIcon = true,
onLeftIconClick = onBackClick,
@@ -49,6 +68,17 @@ fun SettingScreen(
)
SettingDivider()
+ SettingListItem(
+ title = stringResource(R.string.setting_list_notification),
+ onClick = { context.openNotificationSettings() },
+ badge = if (notificationsEnabled) {
+ SettingListItemBadge.On
+ } else {
+ SettingListItemBadge.Off
+ },
+ )
+ SettingDivider()
+
Column(verticalArrangement = Arrangement.spacedBy(GamssTheme.spacing.spacing300)) {
if (SHOW_SERVICE_TERMS) {
SettingListItem(
@@ -80,5 +110,23 @@ private fun SettingDivider() {
private fun Context.appVersion(): String =
packageManager.getPackageInfo(packageName, 0).versionName.orEmpty()
+private fun Context.notificationsEnabled(): Boolean =
+ NotificationManagerCompat.from(this).areNotificationsEnabled()
+
+/**
+ * 앱 내에 알림 토글을 두지 않고 OS의 앱별 알림 설정으로 보낸다.
+ */
+private fun Context.openNotificationSettings() {
+ try {
+ startActivity(
+ Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
+ .putExtra(Settings.EXTRA_APP_PACKAGE, packageName),
+ )
+ } catch (_: ActivityNotFoundException) {
+ // 앱별 알림 설정 화면이 없는 기기를 위해 앱 정보 화면으로 대체한다.
+ startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, "package:$packageName".toUri()))
+ }
+}
+
// 약관과 처리방침이 한 문서에 함께 실려 있다. 문서가 분리되면 true 로 되돌린다.
private const val SHOW_SERVICE_TERMS = false
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
index 3c4e80ba..d8aa41fe 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
@@ -1,6 +1,7 @@
package com.gamss.android.feature.setting.nicknamechange
import android.widget.Toast
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -58,9 +59,12 @@ fun NicknameChangeScreen(
Column(
modifier = modifier
.fillMaxSize()
+ .background(GamssTheme.colors.background)
.imePadding(),
) {
GamssTopNavigation(
+ // 기본값은 기기의 다크 모드를 보므로, 라이트로 고정된 앱 테마와 어긋나지 않게 직접 넘긴다.
+ backgroundColor = GamssTheme.colors.background,
title = stringResource(R.string.account_info_nickname_change),
showLeftIcon = true,
onLeftIconClick = onBackClick,
@@ -69,11 +73,9 @@ fun NicknameChangeScreen(
GamssTextField(
modifier = Modifier
.fillMaxWidth()
- .padding(
- horizontal = 18.dp,
- vertical = GamssTheme.spacing.spacing400,
- ),
- label = stringResource(R.string.account_info_nickname_change),
+ .padding(horizontal = ScreenHorizontalPadding)
+ .padding(top = GamssTheme.spacing.spacing300),
+ label = stringResource(R.string.nickname_change_input_label),
value = state.nicknameInput,
onValueChange = { nickname ->
if (nickname.length <= NicknamePolicy.MAX_LENGTH) {
@@ -87,7 +89,8 @@ fun NicknameChangeScreen(
GamssButton(
modifier = Modifier
.fillMaxWidth()
- .padding(18.dp),
+ .padding(horizontal = ScreenHorizontalPadding)
+ .padding(bottom = GamssTheme.spacing.spacing200),
label = stringResource(R.string.nickname_change_save),
onClick = viewModel::saveNickname,
variant = GamssButtonVariant.Primary,
@@ -96,6 +99,8 @@ fun NicknameChangeScreen(
}
}
+private val ScreenHorizontalPadding = 18.dp
+
private fun NicknameFailureReason.toMessage(): String =
when (this) {
NicknameFailureReason.MISSING -> "닉네임을 입력해주세요"
diff --git a/feature/setting/src/main/res/values/strings.xml b/feature/setting/src/main/res/values/strings.xml
index 4b9ec673..de890b09 100644
--- a/feature/setting/src/main/res/values/strings.xml
+++ b/feature/setting/src/main/res/values/strings.xml
@@ -2,6 +2,9 @@
설정
계정 정보
+ 알림 설정
+ ON
+ OFF
서비스 이용약관
개인정보 처리방침
앱 버전
@@ -16,5 +19,6 @@
회원 탈퇴 시 지금까지 기록된 카드와 대화 내용은\n영원히 삭제되며 복구되지 않아요.
탈퇴하기
뒤로가기
+ 변경할 닉네임을 입력해주세요.
저장하기
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e3b3c180..4b6963f1 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -46,6 +46,7 @@ paging = "3.4.2"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt" }
+androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3-android", version.ref = "lifecycle-viewmodel-navigation3" }
From 0bffe807e1e1f27ed82196c706ffc678e865ca0e Mon Sep 17 00:00:00 2001
From: soyeonLee <109227292+soyeonLee126@users.noreply.github.com>
Date: Tue, 18 Aug 2026 11:30:10 +0900
Subject: [PATCH 2/8] =?UTF-8?q?fix:=20=EB=8B=A4=ED=81=AC=20=EB=AA=A8?=
=?UTF-8?q?=EB=93=9C=20=EA=B8=B0=EA=B8=B0=EC=97=90=EC=84=9C=20=EC=95=B1=20?=
=?UTF-8?q?=EC=85=B8=EA=B3=BC=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=B0=94=20?=
=?UTF-8?q?=EC=83=89=EC=9D=B4=20=EC=96=B4=EA=B8=8B=EB=82=98=EB=8A=94=20?=
=?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- GamssTopNavigation 의 배경 기본값과 로고 분기를 isSystemInDarkTheme() 대신
앱 테마 기준으로 바꿈
셸이 라이트로 고정된 동안 기기 다크 모드를 직접 보면 흰 배경에 검은 상단바가 나왔다
- 판정에 쓸 GamssColors.isDark 토큰 추가
- 호출부 5곳에서 기본값을 우회하던 backgroundColor 인자와 주석 제거
- MainActivity 의 테마도 라이트로 고정
시스템 바 아이콘만 라이트로 고정돼 있어 다크 모드 기기의 로그인/온보딩 화면에서
검은 배경에 검은 아이콘이 겹쳤다
- values-night 윈도우 배경도 라이트로 통일해 첫 프레임 깜빡임 제거
- Material 색 스킴에 primary/onPrimary 를 지정
CircularProgressIndicator 가 M3 기본 보라를 쓰고 있었다
---
.../kotlin/com/gamss/android/app/MainActivity.kt | 7 +++----
app/src/main/res/values-night/themes.xml | 3 ++-
.../android/core/designsystem/theme/Color.kt | 5 +++++
.../android/core/designsystem/theme/Theme.kt | 14 ++++++++++++--
.../topnavigation/GamssTopNavigation.kt | 15 +++------------
.../feature/chat/component/ChattingListTopBar.kt | 1 -
.../feature/onboarding/OnboardingScreen.kt | 1 -
.../setting/accountinfo/AccountInfoScreen.kt | 2 --
.../android/feature/setting/main/SettingScreen.kt | 2 --
.../nicknamechange/NicknameChangeScreen.kt | 2 --
10 files changed, 25 insertions(+), 27 deletions(-)
diff --git a/app/src/main/kotlin/com/gamss/android/app/MainActivity.kt b/app/src/main/kotlin/com/gamss/android/app/MainActivity.kt
index a830070e..fe954f99 100644
--- a/app/src/main/kotlin/com/gamss/android/app/MainActivity.kt
+++ b/app/src/main/kotlin/com/gamss/android/app/MainActivity.kt
@@ -15,15 +15,14 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- // 인자 없는 기본값은 시스템 바 아이콘 색을 기기 다크 모드에 맞춘다. 하지만 셸이 라이트로
- // 고정돼 있어(MainScreen 참고) 기기가 다크면 흰 배경에 흰 아이콘이 겹친다. 다크 시안이
- // 나와 셸 고정을 풀 때 이 스타일도 함께 되돌린다.
+ // 시스템 바 아이콘과 테마를 모두 라이트로 고정한다. 둘 중 하나만 고정하면 다크 모드 기기에서
+ // 검은 배경에 검은 아이콘이 겹친다. 다크 시안이 나오면 이 고정과 MainScreen 의 고정을 함께 푼다.
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.BLACK),
navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.BLACK),
)
setContent {
- GamssTheme {
+ GamssTheme(darkTheme = false) {
GamssRootNavHost()
}
}
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index e53a9879..869ce45b 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,4 +1,5 @@
-
+
+
diff --git a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/theme/Color.kt
index c6b979ab..7b051286 100644
--- a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/theme/Color.kt
+++ b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/theme/Color.kt
@@ -59,6 +59,9 @@ internal val ChromaticDarkPurple = Color(0xFFCC6DFC)
@Immutable
data class GamssColors(
+ // 기기 다크 모드가 아니라 GamssTheme 이 고른 팔레트를 가리킨다. 셸이 라이트로 고정된 동안
+ // isSystemInDarkTheme() 을 직접 보면 테마와 어긋나므로, 다크 분기는 이 값으로 판단한다.
+ val isDark: Boolean,
val white: Color,
val black: Color,
// Figma 의 Gray/Gray1000. 라이트와 다크가 같은 값이라 gray025~gray950 과 달리 반전하지 않는다.
@@ -88,6 +91,7 @@ data class GamssColors(
)
val LightGamssColors = GamssColors(
+ isDark = false,
white = White,
black = Black,
gray1000 = Gray1000,
@@ -115,6 +119,7 @@ val LightGamssColors = GamssColors(
)
val DarkGamssColors = GamssColors(
+ isDark = true,
white = White,
black = Black,
gray1000 = Gray1000,
diff --git a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/theme/Theme.kt
index 0b52eb6b..d25ae190 100644
--- a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/theme/Theme.kt
+++ b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/theme/Theme.kt
@@ -13,10 +13,20 @@ fun GamssTheme(
content: @Composable () -> Unit,
) {
val colors = if (darkTheme) DarkGamssColors else LightGamssColors
+ // 디자인 시스템에 primary 토큰이 없어 Primary 버튼과 같은 gray950 을 쓴다.
+ // CircularProgressIndicator 등 Material 기본값이 이 색을 참조한다.
val materialColorScheme = if (darkTheme) {
- darkColorScheme(background = colors.gray025)
+ darkColorScheme(
+ background = colors.gray025,
+ primary = colors.gray950,
+ onPrimary = colors.gray025,
+ )
} else {
- lightColorScheme(background = colors.gray025)
+ lightColorScheme(
+ background = colors.gray025,
+ primary = colors.gray950,
+ onPrimary = colors.gray025,
+ )
}
CompositionLocalProvider(LocalGamssColors provides colors) {
diff --git a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/topnavigation/GamssTopNavigation.kt b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/topnavigation/GamssTopNavigation.kt
index 4c11ea12..a2705b79 100644
--- a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/topnavigation/GamssTopNavigation.kt
+++ b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/topnavigation/GamssTopNavigation.kt
@@ -4,7 +4,6 @@ import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -70,11 +69,7 @@ sealed interface GamssTopNavigationContent {
fun GamssTopNavigation(
content: GamssTopNavigationContent,
modifier: Modifier = Modifier,
- backgroundColor: Color = if (isSystemInDarkTheme()) {
- GamssTheme.colors.black
- } else {
- GamssTheme.colors.white
- },
+ backgroundColor: Color = GamssTheme.colors.background,
showLeftIcon: Boolean = false,
showRightIcon: Boolean = false,
leftIconContentDescription: String? = null,
@@ -151,11 +146,7 @@ fun GamssTopNavigation(
title: String,
modifier: Modifier = Modifier,
titleAlignment: GamssTopNavigationTitleAlignment = GamssTopNavigationTitleAlignment.Start,
- backgroundColor: Color = if (isSystemInDarkTheme()) {
- GamssTheme.colors.black
- } else {
- GamssTheme.colors.white
- },
+ backgroundColor: Color = GamssTheme.colors.background,
showLeftIcon: Boolean = false,
showRightIcon: Boolean = false,
leftIconContentDescription: String? = null,
@@ -192,7 +183,7 @@ private fun TopNavigationContent(
GamssTopNavigationContent.Logo -> Image(
modifier = modifier.size(width = LogoWidth, height = LogoHeight),
painter = painterResource(
- if (isSystemInDarkTheme()) {
+ if (GamssTheme.colors.isDark) {
R.drawable.ic_logo_dark
} else {
R.drawable.ic_logo_light
diff --git a/feature/chat/src/main/java/com/gamss/android/feature/chat/component/ChattingListTopBar.kt b/feature/chat/src/main/java/com/gamss/android/feature/chat/component/ChattingListTopBar.kt
index 533ba66c..c75c538c 100644
--- a/feature/chat/src/main/java/com/gamss/android/feature/chat/component/ChattingListTopBar.kt
+++ b/feature/chat/src/main/java/com/gamss/android/feature/chat/component/ChattingListTopBar.kt
@@ -40,7 +40,6 @@ internal fun ChattingListTopBar(
} else {
GamssTopNavigationContent.Logo
},
- backgroundColor = GamssTheme.colors.background,
showLeftIcon = isSelectionMode,
showRightIcon = true,
leftIconContentDescription = stringResource(R.string.chatting_list_selection_cancel),
diff --git a/feature/onboarding/src/main/java/com/gamss/android/feature/onboarding/OnboardingScreen.kt b/feature/onboarding/src/main/java/com/gamss/android/feature/onboarding/OnboardingScreen.kt
index 3a66dcde..b56cab05 100644
--- a/feature/onboarding/src/main/java/com/gamss/android/feature/onboarding/OnboardingScreen.kt
+++ b/feature/onboarding/src/main/java/com/gamss/android/feature/onboarding/OnboardingScreen.kt
@@ -203,7 +203,6 @@ private fun OnboardingTopBar(
Box(modifier = modifier.fillMaxWidth()) {
GamssTopNavigation(
content = GamssTopNavigationContent.None,
- backgroundColor = GamssTheme.colors.background,
showLeftIcon = showBack,
leftIconContentDescription = stringResource(R.string.onboarding_back_description),
onLeftIconClick = onBackClick,
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/accountinfo/AccountInfoScreen.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/accountinfo/AccountInfoScreen.kt
index cf17d224..e258580e 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/accountinfo/AccountInfoScreen.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/accountinfo/AccountInfoScreen.kt
@@ -77,8 +77,6 @@ fun AccountInfoScreen(
) {
GamssTopNavigation(
modifier = Modifier.padding(bottom = GamssTheme.spacing.spacing300),
- // 기본값은 기기의 다크 모드를 보므로, 라이트로 고정된 앱 테마와 어긋나지 않게 직접 넘긴다.
- backgroundColor = GamssTheme.colors.background,
title = stringResource(R.string.setting_list_user_account),
showLeftIcon = true,
onLeftIconClick = onBackClick,
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt
index 945fdfdc..af938c5c 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt
@@ -55,8 +55,6 @@ fun SettingScreen(
) {
GamssTopNavigation(
modifier = Modifier.padding(bottom = GamssTheme.spacing.spacing300),
- // 기본값은 기기의 다크 모드를 보므로, 라이트로 고정된 앱 테마와 어긋나지 않게 직접 넘긴다.
- backgroundColor = GamssTheme.colors.background,
title = stringResource(R.string.setting_title),
showLeftIcon = true,
onLeftIconClick = onBackClick,
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
index d8aa41fe..f7299f1d 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
@@ -63,8 +63,6 @@ fun NicknameChangeScreen(
.imePadding(),
) {
GamssTopNavigation(
- // 기본값은 기기의 다크 모드를 보므로, 라이트로 고정된 앱 테마와 어긋나지 않게 직접 넘긴다.
- backgroundColor = GamssTheme.colors.background,
title = stringResource(R.string.account_info_nickname_change),
showLeftIcon = true,
onLeftIconClick = onBackClick,
From 48bbe720c1b4dcf9073d6d7b171b5fe5f45b711a Mon Sep 17 00:00:00 2001
From: soyeonLee <109227292+soyeonLee126@users.noreply.github.com>
Date: Tue, 18 Aug 2026 11:30:18 +0900
Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?=
=?UTF-8?q?=EC=9E=85=EB=A0=A5=20=ED=95=84=EB=93=9C=EC=97=90=20placeholder?=
=?UTF-8?q?=20=EC=99=80=20=EC=B5=9C=EC=86=8C=20=EA=B8=B8=EC=9D=B4=20?=
=?UTF-8?q?=EC=95=88=EB=82=B4=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- GamssTextField 에 placeholder, errorMessage 파라미터 추가
placeholder 는 값이 비었을 때만, 안내 문구는 필드 아래에 시안 색/타이포로 그린다
- 닉네임 변경 화면에 두 문구 연결
안내 문구는 한 글자라도 입력한 뒤 공백 제외 길이가 최소 길이 미만일 때 뜬다
- 입력 길이 제한 기준을 원본에서 공백 제외 길이로 변경
저장 판정과 기준이 달라 뒤에 공백이 붙으면 유효 글자를 다 채우지 못했다
---
.../designsystem/textfield/GamssTextField.kt | 49 +++++++++++++++++++
.../nicknamechange/NicknameChangeScreen.kt | 10 +++-
.../setting/src/main/res/values/strings.xml | 2 +
3 files changed, 60 insertions(+), 1 deletion(-)
diff --git a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt
index 3d540c18..705333cf 100644
--- a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt
+++ b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt
@@ -32,6 +32,8 @@ fun GamssTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
+ placeholder: String? = null,
+ errorMessage: String? = null,
singleLine: Boolean = true,
) {
Column(modifier = modifier) {
@@ -58,9 +60,24 @@ fun GamssTextField(
.padding(horizontal = GamssTheme.spacing.spacing300),
contentAlignment = Alignment.CenterStart,
) {
+ if (placeholder != null && value.isEmpty()) {
+ Text(
+ text = placeholder,
+ style = GamssTheme.typography.body3Medium,
+ color = GamssTheme.colors.gray400,
+ )
+ }
innerTextField()
}
}
+ if (errorMessage != null) {
+ Spacer(modifier = Modifier.height(GamssTheme.spacing.spacing100))
+ Text(
+ text = errorMessage,
+ style = GamssTheme.typography.body5Medium,
+ color = GamssTheme.colors.red,
+ )
+ }
}
}
@@ -75,8 +92,40 @@ private fun GamssTextFieldPreview() {
GamssTextField(
modifier = Modifier.fillMaxWidth(),
label = "변경할 닉네임을 입력해주세요.",
+ placeholder = "닉네임은 2~20자 사이로 입력해주세요.",
value = "송지연",
onValueChange = {},
)
}
}
+
+@Preview(name = "GamssTextField - Placeholder", showBackground = true)
+@Composable
+@Suppress("UnusedPrivateMember")
+private fun GamssTextFieldPlaceholderPreview() {
+ GamssTheme {
+ GamssTextField(
+ modifier = Modifier.fillMaxWidth(),
+ label = "변경할 닉네임을 입력해주세요.",
+ placeholder = "닉네임은 2~20자 사이로 입력해주세요.",
+ value = "",
+ onValueChange = {},
+ )
+ }
+}
+
+@Preview(name = "GamssTextField - Error", showBackground = true)
+@Composable
+@Suppress("UnusedPrivateMember")
+private fun GamssTextFieldErrorPreview() {
+ GamssTheme {
+ GamssTextField(
+ modifier = Modifier.fillMaxWidth(),
+ label = "변경할 닉네임을 입력해주세요.",
+ placeholder = "닉네임은 2~20자 사이로 입력해주세요.",
+ errorMessage = "닉네임은 2자 이상으로 입력해주세요.",
+ value = "아",
+ onValueChange = {},
+ )
+ }
+}
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
index f7299f1d..8557653c 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
@@ -55,6 +55,9 @@ fun NicknameChangeScreen(
val canSave = trimmedInput.isNotEmpty() &&
trimmedInput != state.originalNickname &&
trimmedInput.length in NicknamePolicy.MIN_LENGTH..NicknamePolicy.MAX_LENGTH
+ // 최대 길이는 onValueChange 에서 이미 막으므로, 최소 길이만 안내한다. 판정 기준은 canSave 와 같은 공백 제외 길이다.
+ val showMinLengthError = state.nicknameInput.isNotEmpty() &&
+ trimmedInput.length < NicknamePolicy.MIN_LENGTH
Column(
modifier = modifier
@@ -74,9 +77,14 @@ fun NicknameChangeScreen(
.padding(horizontal = ScreenHorizontalPadding)
.padding(top = GamssTheme.spacing.spacing300),
label = stringResource(R.string.nickname_change_input_label),
+ placeholder = stringResource(R.string.nickname_change_input_placeholder),
+ errorMessage = stringResource(R.string.nickname_change_input_error_min_length)
+ .takeIf { showMinLengthError },
value = state.nicknameInput,
onValueChange = { nickname ->
- if (nickname.length <= NicknamePolicy.MAX_LENGTH) {
+ // 저장 판정과 같은 공백 제외 길이로 막는다. 원본 길이로 재면 뒤에 붙은 공백 때문에
+ // 유효 글자를 20자까지 채우지 못한다.
+ if (nickname.trim().length <= NicknamePolicy.MAX_LENGTH) {
viewModel.onNicknameInputChange(nickname)
}
},
diff --git a/feature/setting/src/main/res/values/strings.xml b/feature/setting/src/main/res/values/strings.xml
index de890b09..dd570702 100644
--- a/feature/setting/src/main/res/values/strings.xml
+++ b/feature/setting/src/main/res/values/strings.xml
@@ -20,5 +20,7 @@
탈퇴하기
뒤로가기
변경할 닉네임을 입력해주세요.
+ 닉네임은 2~20자 사이로 입력해주세요.
+ 닉네임은 2자 이상으로 입력해주세요.
저장하기
From ae192761120dbc477118d6dd302dcbb606ed84ab Mon Sep 17 00:00:00 2001
From: soyeonLee <109227292+soyeonLee126@users.noreply.github.com>
Date: Tue, 18 Aug 2026 11:30:26 +0900
Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=EC=84=A4=EC=A0=95=20=ED=99=94?=
=?UTF-8?q?=EB=A9=B4=20=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20=EC=A7=84?=
=?UTF-8?q?=EC=9E=85=20=ED=8F=B4=EB=B0=B1=EA=B3=BC=20=EB=B0=B0=EC=A7=80=20?=
=?UTF-8?q?=EC=9E=98=EB=A6=BC=20=EB=B0=A9=EC=96=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 앱 정보 화면 폴백도 runCatching 으로 감쌈
두 설정 화면이 모두 없는 기기에서 폴백이 그대로 터져 앱이 죽을 수 있었다
- 설정 목록 항목의 제목에 weight 를 줘 배지가 밀려 잘리지 않도록 함
제목이 길거나 글꼴이 커지면 알림 상태를 알리는 ON/OFF 배지가 사라졌다
---
.../android/feature/setting/component/SettingListItem.kt | 2 ++
.../gamss/android/feature/setting/main/SettingScreen.kt | 7 +++++--
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt
index 23aa808d..37a9d1e3 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt
@@ -38,6 +38,8 @@ fun SettingListItem(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
+ // 제목이 길어도 배지가 밀려 잘리지 않도록, 남는 폭만 제목이 가져간다.
+ modifier = Modifier.weight(1f, fill = false),
text = title,
style = GamssTheme.typography.body3Medium,
color = contentColor,
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt
index af938c5c..b5384978 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/main/SettingScreen.kt
@@ -121,8 +121,11 @@ private fun Context.openNotificationSettings() {
.putExtra(Settings.EXTRA_APP_PACKAGE, packageName),
)
} catch (_: ActivityNotFoundException) {
- // 앱별 알림 설정 화면이 없는 기기를 위해 앱 정보 화면으로 대체한다.
- startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, "package:$packageName".toUri()))
+ // 앱별 알림 설정 화면이 없는 기기를 위해 앱 정보 화면으로 대체한다. 둘 다 없는 기기도 있어
+ // 폴백 실패는 무시한다. 설정 진입 실패로 앱이 죽어서는 안 된다.
+ runCatching {
+ startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, "package:$packageName".toUri()))
+ }
}
}
From 5d79ffd2f9fe7859006263e8576232ca542bd972 Mon Sep 17 00:00:00 2001
From: soyeonLee <109227292+soyeonLee126@users.noreply.github.com>
Date: Wed, 19 Aug 2026 00:00:31 +0900
Subject: [PATCH 5/8] =?UTF-8?q?fix:=20=EB=B2=84=EC=A0=84=20=EC=B9=B4?=
=?UTF-8?q?=ED=83=88=EB=A1=9C=EA=B7=B8=20=EC=A4=91=EB=B3=B5=20=ED=82=A4=20?=
=?UTF-8?q?=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
develop 병합 시 양쪽에서 같은 항목을 추가해 androidx-lifecycle-runtime-compose 가
두 번 정의되어 TOML 파싱이 실패했다.
---
gradle/libs.versions.toml | 1 -
1 file changed, 1 deletion(-)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9991db68..8d7855c4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -48,7 +48,6 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidx-hilt" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
-androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3-android", version.ref = "lifecycle-viewmodel-navigation3" }
From 8c876584ba307cbc465c25f9d44723e7298593d6 Mon Sep 17 00:00:00 2001
From: soyeonLee <109227292+soyeonLee126@users.noreply.github.com>
Date: Wed, 19 Aug 2026 00:00:52 +0900
Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?=
=?UTF-8?q?=EB=B3=80=EA=B2=BD=20=ED=99=94=EB=A9=B4=20=EC=9E=85=EB=A0=A5=20?=
=?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=8B=9C=EC=95=88=20=EB=B0=98=EC=98=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 입력 필드 테두리를 시안의 손그림 벡터로 교체하고 gray950 으로 틴트해 다크 모드에서도
색만 반전되게 한다. pathData 는 aapt2 문자열 길이 한계(32767자)를 넘어 path 를 나눴다.
- 최대 길이를 10자로 줄이고, 초과 입력을 막는 대신 안내 문구를 띄우고 저장 버튼을 막는다.
- 키보드가 올라올 때 내비게이션 바 inset 이 이중으로 더해지던 여백을 없앤다.
---
.../designsystem/textfield/GamssTextField.kt | 48 ++++++++++++-------
.../res/drawable/bg_textfield_default.xml | 27 +++++++++++
.../android/domain/user/NicknamePolicy.kt | 2 +-
.../nicknamechange/NicknameChangeScreen.kt | 37 ++++++++------
.../setting/src/main/res/values/strings.xml | 3 +-
5 files changed, 84 insertions(+), 33 deletions(-)
create mode 100644 core/designsystem/src/main/res/drawable/bg_textfield_default.xml
diff --git a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt
index 705333cf..ba96580a 100644
--- a/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt
+++ b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt
@@ -1,7 +1,7 @@
package com.gamss.android.core.designsystem.textfield
+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.Column
import androidx.compose.foundation.layout.Spacer
@@ -14,17 +14,24 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import com.gamss.android.core.designsystem.R
import com.gamss.android.core.designsystem.theme.GamssTheme
/**
* 라벨 텍스트 + 입력 필드 조합의 입력 필드.
* 라벨이 필드 위에 별도로 표시되는 형태
*
- * 시안의 필드는 모서리가 직각이고 높이가 52dp라, 최소 높이 56dp에 자체 여백을 가진
+ * 시안의 필드는 높이가 52dp라, 최소 높이 56dp에 자체 여백을 가진
* [androidx.compose.material3.OutlinedTextField] 대신 [BasicTextField]로 그린다.
+ *
+ * 테두리는 손그림 시안이라 선 두께로 그릴 수 없어 `bg_textfield_default` 벡터를 깔고
+ * gray950 으로 틴트한다. 덕분에 다크 모드에서도 색만 반전된다.
*/
@Composable
fun GamssTextField(
@@ -55,19 +62,29 @@ fun GamssTextField(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = FieldMinHeight)
- .background(GamssTheme.colors.gray025)
- .border(width = FieldBorderWidth, color = GamssTheme.colors.gray950)
- .padding(horizontal = GamssTheme.spacing.spacing300),
+ .background(GamssTheme.colors.gray025),
contentAlignment = Alignment.CenterStart,
) {
- if (placeholder != null && value.isEmpty()) {
- Text(
- text = placeholder,
- style = GamssTheme.typography.body3Medium,
- color = GamssTheme.colors.gray400,
- )
+ // matchParentSize 는 부모 크기 계산에 참여하지 않아, 필드 높이는 그대로 내용이
+ // 정하고 테두리만 정해진 크기에 맞춰 늘어난다. Modifier.paint 는 반대로
+ // 남은 공간을 꽉 채우도록 제약을 고정해 버려 쓸 수 없다.
+ Image(
+ modifier = Modifier.matchParentSize(),
+ painter = painterResource(R.drawable.bg_textfield_default),
+ contentDescription = null,
+ contentScale = ContentScale.FillBounds,
+ colorFilter = ColorFilter.tint(GamssTheme.colors.gray950),
+ )
+ Box(modifier = Modifier.padding(horizontal = GamssTheme.spacing.spacing300)) {
+ if (placeholder != null && value.isEmpty()) {
+ Text(
+ text = placeholder,
+ style = GamssTheme.typography.body3Medium,
+ color = GamssTheme.colors.gray400,
+ )
+ }
+ innerTextField()
}
- innerTextField()
}
}
if (errorMessage != null) {
@@ -82,7 +99,6 @@ fun GamssTextField(
}
private val FieldMinHeight = 52.dp
-private val FieldBorderWidth = 1.dp
@Preview(name = "GamssTextField", showBackground = true)
@Composable
@@ -92,7 +108,7 @@ private fun GamssTextFieldPreview() {
GamssTextField(
modifier = Modifier.fillMaxWidth(),
label = "변경할 닉네임을 입력해주세요.",
- placeholder = "닉네임은 2~20자 사이로 입력해주세요.",
+ placeholder = "닉네임은 2~10자 사이로 입력해주세요.",
value = "송지연",
onValueChange = {},
)
@@ -107,7 +123,7 @@ private fun GamssTextFieldPlaceholderPreview() {
GamssTextField(
modifier = Modifier.fillMaxWidth(),
label = "변경할 닉네임을 입력해주세요.",
- placeholder = "닉네임은 2~20자 사이로 입력해주세요.",
+ placeholder = "닉네임은 2~10자 사이로 입력해주세요.",
value = "",
onValueChange = {},
)
@@ -122,7 +138,7 @@ private fun GamssTextFieldErrorPreview() {
GamssTextField(
modifier = Modifier.fillMaxWidth(),
label = "변경할 닉네임을 입력해주세요.",
- placeholder = "닉네임은 2~20자 사이로 입력해주세요.",
+ placeholder = "닉네임은 2~10자 사이로 입력해주세요.",
errorMessage = "닉네임은 2자 이상으로 입력해주세요.",
value = "아",
onValueChange = {},
diff --git a/core/designsystem/src/main/res/drawable/bg_textfield_default.xml b/core/designsystem/src/main/res/drawable/bg_textfield_default.xml
new file mode 100644
index 00000000..9af54180
--- /dev/null
+++ b/core/designsystem/src/main/res/drawable/bg_textfield_default.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
diff --git a/domain/src/main/kotlin/com/gamss/android/domain/user/NicknamePolicy.kt b/domain/src/main/kotlin/com/gamss/android/domain/user/NicknamePolicy.kt
index 74d3638e..307e7d3d 100644
--- a/domain/src/main/kotlin/com/gamss/android/domain/user/NicknamePolicy.kt
+++ b/domain/src/main/kotlin/com/gamss/android/domain/user/NicknamePolicy.kt
@@ -6,5 +6,5 @@ package com.gamss.android.domain.user
*/
object NicknamePolicy {
const val MIN_LENGTH = 2
- const val MAX_LENGTH = 20
+ const val MAX_LENGTH = 10
}
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
index 8557653c..b7c2e164 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
@@ -4,10 +4,14 @@ import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -55,15 +59,25 @@ fun NicknameChangeScreen(
val canSave = trimmedInput.isNotEmpty() &&
trimmedInput != state.originalNickname &&
trimmedInput.length in NicknamePolicy.MIN_LENGTH..NicknamePolicy.MAX_LENGTH
- // 최대 길이는 onValueChange 에서 이미 막으므로, 최소 길이만 안내한다. 판정 기준은 canSave 와 같은 공백 제외 길이다.
- val showMinLengthError = state.nicknameInput.isNotEmpty() &&
- trimmedInput.length < NicknamePolicy.MIN_LENGTH
+ // 길이를 벗어난 입력도 막지 않고 그대로 받아 안내 문구로 알린다. 저장은 canSave 가 막는다.
+ // 판정 기준은 canSave 와 같은 공백 제외 길이다.
+ val lengthErrorMessage = when {
+ state.nicknameInput.isEmpty() -> null
+
+ trimmedInput.length < NicknamePolicy.MIN_LENGTH ->
+ stringResource(R.string.nickname_change_input_error_min_length)
+
+ trimmedInput.length > NicknamePolicy.MAX_LENGTH ->
+ stringResource(R.string.nickname_change_input_error_max_length)
+
+ else -> null
+ }
Column(
modifier = modifier
.fillMaxSize()
.background(GamssTheme.colors.background)
- .imePadding(),
+ .windowInsetsPadding(WindowInsets.ime.exclude(WindowInsets.navigationBars)),
) {
GamssTopNavigation(
title = stringResource(R.string.account_info_nickname_change),
@@ -78,16 +92,9 @@ fun NicknameChangeScreen(
.padding(top = GamssTheme.spacing.spacing300),
label = stringResource(R.string.nickname_change_input_label),
placeholder = stringResource(R.string.nickname_change_input_placeholder),
- errorMessage = stringResource(R.string.nickname_change_input_error_min_length)
- .takeIf { showMinLengthError },
+ errorMessage = lengthErrorMessage,
value = state.nicknameInput,
- onValueChange = { nickname ->
- // 저장 판정과 같은 공백 제외 길이로 막는다. 원본 길이로 재면 뒤에 붙은 공백 때문에
- // 유효 글자를 20자까지 채우지 못한다.
- if (nickname.trim().length <= NicknamePolicy.MAX_LENGTH) {
- viewModel.onNicknameInputChange(nickname)
- }
- },
+ onValueChange = viewModel::onNicknameInputChange,
)
Spacer(modifier = Modifier.weight(1f))
@@ -110,7 +117,7 @@ private val ScreenHorizontalPadding = 18.dp
private fun NicknameFailureReason.toMessage(): String =
when (this) {
NicknameFailureReason.MISSING -> "닉네임을 입력해주세요"
- NicknameFailureReason.INVALID_LENGTH -> "닉네임은 2~20자로 입력해주세요"
+ NicknameFailureReason.INVALID_LENGTH -> "닉네임은 2~10자로 입력해주세요"
NicknameFailureReason.INVALID_NICKNAME -> "사용할 수 없는 닉네임이에요"
NicknameFailureReason.NETWORK -> "네트워크 연결을 확인해주세요"
NicknameFailureReason.UNKNOWN -> "닉네임 변경에 실패했어요"
diff --git a/feature/setting/src/main/res/values/strings.xml b/feature/setting/src/main/res/values/strings.xml
index dd570702..5fb7cfc2 100644
--- a/feature/setting/src/main/res/values/strings.xml
+++ b/feature/setting/src/main/res/values/strings.xml
@@ -20,7 +20,8 @@
탈퇴하기
뒤로가기
변경할 닉네임을 입력해주세요.
- 닉네임은 2~20자 사이로 입력해주세요.
+ 닉네임은 2~10자 사이로 입력해주세요.
닉네임은 2자 이상으로 입력해주세요.
+ 닉네임은 10자 이하로 입력해주세요.
저장하기
From 83606e495c305513867ed572094d23ebc7f998e7 Mon Sep 17 00:00:00 2001
From: soyeonLee <109227292+soyeonLee126@users.noreply.github.com>
Date: Wed, 19 Aug 2026 00:13:18 +0900
Subject: [PATCH 7/8] =?UTF-8?q?fix:=20=EC=84=A4=EC=A0=95=20=EB=AA=A9?=
=?UTF-8?q?=EB=A1=9D=EC=9D=98=20=EB=B6=80=EA=B0=80=20=EC=A0=95=EB=B3=B4?=
=?UTF-8?q?=EA=B0=80=20=EC=98=A4=EB=A5=B8=EC=AA=BD=20=EB=81=9D=EC=97=90=20?=
=?UTF-8?q?=EB=B6=99=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
제목과 Spacer 가 남는 폭을 1:1 로 나눠 가져, 제목이 짧아도 남은 몫이 Spacer 로
넘어가지 않아 앱 버전이 끝까지 밀리지 않았다. 제목과 배지를 weight 를 가진
Row 로 묶어 남는 폭을 모두 차지하게 한다.
---
.../setting/component/SettingListItem.kt | 32 ++++++++++---------
1 file changed, 17 insertions(+), 15 deletions(-)
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt
index 37a9d1e3..5dec296d 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/component/SettingListItem.kt
@@ -4,7 +4,6 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
@@ -37,22 +36,25 @@ fun SettingListItem(
.padding(contentPadding),
verticalAlignment = Alignment.CenterVertically,
) {
- Text(
- // 제목이 길어도 배지가 밀려 잘리지 않도록, 남는 폭만 제목이 가져간다.
- modifier = Modifier.weight(1f, fill = false),
- text = title,
- style = GamssTheme.typography.body3Medium,
- color = contentColor,
- )
-
- if (badge != null) {
- BadgeChip(
- modifier = Modifier.padding(start = GamssTheme.spacing.spacing050),
- badge = badge,
+ Row(
+ modifier = Modifier.weight(1f),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ // 제목이 길어도 배지가 밀려 잘리지 않도록, 남는 폭만 제목이 가져간다.
+ modifier = Modifier.weight(1f, fill = false),
+ text = title,
+ style = GamssTheme.typography.body3Medium,
+ color = contentColor,
)
- }
- Spacer(modifier = Modifier.weight(1f))
+ if (badge != null) {
+ BadgeChip(
+ modifier = Modifier.padding(start = GamssTheme.spacing.spacing050),
+ badge = badge,
+ )
+ }
+ }
if (additionalInfo != null) {
Text(
From c20a6fe804386498cae13fc1102627b8b45f144d Mon Sep 17 00:00:00 2001
From: soyeonLee <109227292+soyeonLee126@users.noreply.github.com>
Date: Wed, 19 Aug 2026 00:41:20 +0900
Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?=
=?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EC=83=81=ED=95=9C=20=EC=B2=98=EB=A6=AC?=
=?UTF-8?q?=EC=99=80=20=EC=A0=80=EC=9E=A5=20=EB=B2=84=ED=8A=BC=20=ED=95=98?=
=?UTF-8?q?=EB=8B=A8=20=EC=97=AC=EB=B0=B1=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 상한을 넘는 입력은 받지 않고, 초과를 시도했을 때만 안내 문구를 띄운다.
차단하면 상태만으로는 초과 시도를 알 수 없어 화면 로컬 플래그로 기억한다.
- placeholder 를 빼고 쓰이지 않게 된 문자열도 지운다.
- 하단 여백을 ime 와 내비게이션 바 중 큰 쪽으로 잡는다. Scaffold 가
contentWindowInsets 를 0 으로 두어 상세 화면에는 내비게이션 바 인셋이
전달되지 않아, 키보드가 내려가면 저장 버튼이 3버튼 내비에 가려졌다.
---
.../nicknamechange/NicknameChangeScreen.kt | 26 ++++++++++++-------
.../setting/src/main/res/values/strings.xml | 1 -
2 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt b/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
index b7c2e164..aa1502ed 100644
--- a/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
+++ b/feature/setting/src/main/java/com/gamss/android/feature/setting/nicknamechange/NicknameChangeScreen.kt
@@ -5,16 +5,19 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.union
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -55,21 +58,20 @@ fun NicknameChangeScreen(
}
}
+ var maxLengthExceeded by rememberSaveable { mutableStateOf(false) }
+
val trimmedInput = state.nicknameInput.trim()
val canSave = trimmedInput.isNotEmpty() &&
trimmedInput != state.originalNickname &&
trimmedInput.length in NicknamePolicy.MIN_LENGTH..NicknamePolicy.MAX_LENGTH
- // 길이를 벗어난 입력도 막지 않고 그대로 받아 안내 문구로 알린다. 저장은 canSave 가 막는다.
- // 판정 기준은 canSave 와 같은 공백 제외 길이다.
val lengthErrorMessage = when {
+ maxLengthExceeded -> stringResource(R.string.nickname_change_input_error_max_length)
+
state.nicknameInput.isEmpty() -> null
trimmedInput.length < NicknamePolicy.MIN_LENGTH ->
stringResource(R.string.nickname_change_input_error_min_length)
- trimmedInput.length > NicknamePolicy.MAX_LENGTH ->
- stringResource(R.string.nickname_change_input_error_max_length)
-
else -> null
}
@@ -77,7 +79,7 @@ fun NicknameChangeScreen(
modifier = modifier
.fillMaxSize()
.background(GamssTheme.colors.background)
- .windowInsetsPadding(WindowInsets.ime.exclude(WindowInsets.navigationBars)),
+ .windowInsetsPadding(WindowInsets.ime.union(WindowInsets.navigationBars)),
) {
GamssTopNavigation(
title = stringResource(R.string.account_info_nickname_change),
@@ -91,10 +93,16 @@ fun NicknameChangeScreen(
.padding(horizontal = ScreenHorizontalPadding)
.padding(top = GamssTheme.spacing.spacing300),
label = stringResource(R.string.nickname_change_input_label),
- placeholder = stringResource(R.string.nickname_change_input_placeholder),
errorMessage = lengthErrorMessage,
value = state.nicknameInput,
- onValueChange = viewModel::onNicknameInputChange,
+ onValueChange = { nickname ->
+ if (nickname.trim().length > NicknamePolicy.MAX_LENGTH) {
+ maxLengthExceeded = true
+ } else {
+ maxLengthExceeded = false
+ viewModel.onNicknameInputChange(nickname)
+ }
+ },
)
Spacer(modifier = Modifier.weight(1f))
diff --git a/feature/setting/src/main/res/values/strings.xml b/feature/setting/src/main/res/values/strings.xml
index 5fb7cfc2..24ee3311 100644
--- a/feature/setting/src/main/res/values/strings.xml
+++ b/feature/setting/src/main/res/values/strings.xml
@@ -20,7 +20,6 @@
탈퇴하기
뒤로가기
변경할 닉네임을 입력해주세요.
- 닉네임은 2~10자 사이로 입력해주세요.
닉네임은 2자 이상으로 입력해주세요.
닉네임은 10자 이하로 입력해주세요.
저장하기