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..fe954f99 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,9 +15,14 @@ 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 {
+ 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/textfield/GamssTextField.kt b/core/designsystem/src/main/java/com/gamss/android/core/designsystem/textfield/GamssTextField.kt
index 9a7faad3..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,20 +1,37 @@
package com.gamss.android.core.designsystem.textfield
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.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.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
/**
- * 라벨 텍스트 + [OutlinedTextField] 조합의 입력 필드.
+ * 라벨 텍스트 + 입력 필드 조합의 입력 필드.
* 라벨이 필드 위에 별도로 표시되는 형태
+ *
+ * 시안의 필드는 높이가 52dp라, 최소 높이 56dp에 자체 여백을 가진
+ * [androidx.compose.material3.OutlinedTextField] 대신 [BasicTextField]로 그린다.
+ *
+ * 테두리는 손그림 시안이라 선 두께로 그릴 수 없어 `bg_textfield_default` 벡터를 깔고
+ * gray950 으로 틴트한다. 덕분에 다크 모드에서도 색만 반전된다.
*/
@Composable
fun GamssTextField(
@@ -22,6 +39,8 @@ fun GamssTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
+ placeholder: String? = null,
+ errorMessage: String? = null,
singleLine: Boolean = true,
) {
Column(modifier = modifier) {
@@ -31,19 +50,56 @@ 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),
+ contentAlignment = Alignment.CenterStart,
+ ) {
+ // 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()
+ }
+ }
+ }
+ if (errorMessage != null) {
+ Spacer(modifier = Modifier.height(GamssTheme.spacing.spacing100))
+ Text(
+ text = errorMessage,
+ style = GamssTheme.typography.body5Medium,
+ color = GamssTheme.colors.red,
+ )
+ }
}
}
+private val FieldMinHeight = 52.dp
+
@Preview(name = "GamssTextField", showBackground = true)
@Composable
@Suppress("UnusedPrivateMember")
@@ -51,9 +107,41 @@ private fun GamssTextFieldPreview() {
GamssTheme {
GamssTextField(
modifier = Modifier.fillMaxWidth(),
- label = "닉네임 변경",
+ label = "변경할 닉네임을 입력해주세요.",
+ placeholder = "닉네임은 2~10자 사이로 입력해주세요.",
value = "송지연",
onValueChange = {},
)
}
}
+
+@Preview(name = "GamssTextField - Placeholder", showBackground = true)
+@Composable
+@Suppress("UnusedPrivateMember")
+private fun GamssTextFieldPlaceholderPreview() {
+ GamssTheme {
+ GamssTextField(
+ modifier = Modifier.fillMaxWidth(),
+ label = "변경할 닉네임을 입력해주세요.",
+ placeholder = "닉네임은 2~10자 사이로 입력해주세요.",
+ value = "",
+ onValueChange = {},
+ )
+ }
+}
+
+@Preview(name = "GamssTextField - Error", showBackground = true)
+@Composable
+@Suppress("UnusedPrivateMember")
+private fun GamssTextFieldErrorPreview() {
+ GamssTheme {
+ GamssTextField(
+ modifier = Modifier.fillMaxWidth(),
+ label = "변경할 닉네임을 입력해주세요.",
+ placeholder = "닉네임은 2~10자 사이로 입력해주세요.",
+ errorMessage = "닉네임은 2자 이상으로 입력해주세요.",
+ value = "아",
+ onValueChange = {},
+ )
+ }
+}
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 9fd7893b..e45f6519 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
@@ -14,10 +14,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(
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 28e04bc7..e20e99c1 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
@@ -96,11 +96,7 @@ sealed interface GamssTopNavigationContent {
fun GamssTopNavigation(
content: GamssTopNavigationContent,
modifier: Modifier = Modifier,
- backgroundColor: Color = if (GamssTheme.isDarkTheme) {
- GamssTheme.colors.black
- } else {
- GamssTheme.colors.white
- },
+ backgroundColor: Color = GamssTheme.colors.background,
showLeftIcon: Boolean = false,
leftIconContentDescription: String? = null,
onLeftIconClick: () -> Unit = {},
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/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 197ea8e5..6f04a21f 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
@@ -41,7 +41,6 @@ internal fun ChattingListTopBar(
} else {
GamssTopNavigationContent.Logo
},
- backgroundColor = GamssTheme.colors.background,
showLeftIcon = isSelectionMode,
leftIconContentDescription = stringResource(R.string.chatting_list_selection_cancel),
onLeftIconClick = onSelectionCancel,
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/build.gradle.kts b/feature/setting/build.gradle.kts
index f479576b..d723147d 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)
}
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..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
@@ -73,10 +73,10 @@ 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),
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..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
@@ -1,18 +1,22 @@
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 +25,7 @@ fun SettingListItem(
contentColor: Color = GamssTheme.colors.gray950,
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
+ badge: SettingListItemBadge? = null,
additionalInfo: String? = null,
contentPadding: PaddingValues = SettingListItemDefaults.ContentPadding,
) {
@@ -31,13 +36,25 @@ fun SettingListItem(
.padding(contentPadding),
verticalAlignment = Alignment.CenterVertically,
) {
- Text(
- text = title,
- style = GamssTheme.typography.body3Medium,
- color = contentColor,
- )
+ 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(
@@ -49,6 +66,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..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
@@ -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,20 @@ 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),
title = stringResource(R.string.setting_title),
showLeftIcon = true,
onLeftIconClick = onBackClick,
@@ -49,6 +66,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 +108,26 @@ 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) {
+ // 앱별 알림 설정 화면이 없는 기기를 위해 앱 정보 화면으로 대체한다. 둘 다 없는 기기도 있어
+ // 폴백 실패는 무시한다. 설정 진입 실패로 앱이 죽어서는 안 된다.
+ runCatching {
+ 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..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
@@ -1,15 +1,23 @@
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.WindowInsets
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.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
@@ -50,15 +58,28 @@ 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
+ 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)
+
+ else -> null
+ }
Column(
modifier = modifier
.fillMaxSize()
- .imePadding(),
+ .background(GamssTheme.colors.background)
+ .windowInsetsPadding(WindowInsets.ime.union(WindowInsets.navigationBars)),
) {
GamssTopNavigation(
title = stringResource(R.string.account_info_nickname_change),
@@ -69,14 +90,16 @@ 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),
+ errorMessage = lengthErrorMessage,
value = state.nicknameInput,
onValueChange = { nickname ->
- if (nickname.length <= NicknamePolicy.MAX_LENGTH) {
+ if (nickname.trim().length > NicknamePolicy.MAX_LENGTH) {
+ maxLengthExceeded = true
+ } else {
+ maxLengthExceeded = false
viewModel.onNicknameInputChange(nickname)
}
},
@@ -87,7 +110,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,10 +120,12 @@ fun NicknameChangeScreen(
}
}
+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 4b9ec673..24ee3311 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,8 @@
회원 탈퇴 시 지금까지 기록된 카드와 대화 내용은\n영원히 삭제되며 복구되지 않아요.
탈퇴하기
뒤로가기
+ 변경할 닉네임을 입력해주세요.
+ 닉네임은 2자 이상으로 입력해주세요.
+ 닉네임은 10자 이하로 입력해주세요.
저장하기
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e5857d18..cea976b4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -46,8 +46,8 @@ 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-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-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" }