diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4e3d769..0cdc27a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { implementation(projects.core.dataLocal) implementation(projects.core.navigation) implementation(projects.core.designsystem) + implementation(projects.feature.mypage) implementation(libs.androidx.navigation3.runtime) implementation(libs.androidx.navigation3.ui) diff --git a/core/data/src/main/java/com/kikidan/data/repository/FakeUserRepository.kt b/core/data/src/main/java/com/kikidan/data/repository/FakeUserRepository.kt index f815cc1..c7d46a3 100644 --- a/core/data/src/main/java/com/kikidan/data/repository/FakeUserRepository.kt +++ b/core/data/src/main/java/com/kikidan/data/repository/FakeUserRepository.kt @@ -16,6 +16,7 @@ class FakeUserRepository Result.success( User( id = "fake-user-id", + name = "토닥이", gender = Gender.FEMALE, birth = Birth( diff --git a/core/designsystem/src/main/java/com/kikidan/designsystem/component/TodakunTab.kt b/core/designsystem/src/main/java/com/kikidan/designsystem/component/TodakunTab.kt index 57df7cd..bcabff4 100644 --- a/core/designsystem/src/main/java/com/kikidan/designsystem/component/TodakunTab.kt +++ b/core/designsystem/src/main/java/com/kikidan/designsystem/component/TodakunTab.kt @@ -24,6 +24,14 @@ import com.kikidan.designsystem.theme.TodakunColor import com.kikidan.designsystem.theme.TodakunTheme import com.kikidan.designsystem.theme.TodakunTypography +/** + * A custom tab component for the Todakun design system. + * + * @param text The text to be displayed on the tab. + * @param selected Whether the tab is currently selected. + * @param onClick Callback to be invoked when the tab is clicked. + * @param modifier The modifier to be applied to the tab. + */ @Composable fun TodakunTab( text: String, @@ -62,6 +70,9 @@ fun TodakunTab( } } +/** + * Default values for [TodakunTab]. + */ private object TodakunTabDefaults { val Shape = RoundedCornerShape(99.dp) } diff --git a/core/designsystem/src/main/res/drawable/ic_chevron_right.xml b/core/designsystem/src/main/res/drawable/ic_chevron_right.xml new file mode 100644 index 0000000..2b42b44 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_chevron_right.xml @@ -0,0 +1,24 @@ + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_logout.xml b/core/designsystem/src/main/res/drawable/ic_logout.xml new file mode 100644 index 0000000..6ca2812 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,39 @@ + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_mail.xml b/core/designsystem/src/main/res/drawable/ic_mail.xml new file mode 100644 index 0000000..152a94a --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_mail.xml @@ -0,0 +1,33 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_manage_saju_info.xml b/core/designsystem/src/main/res/drawable/ic_manage_saju_info.xml new file mode 100644 index 0000000..5886812 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_manage_saju_info.xml @@ -0,0 +1,45 @@ + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_setting.xml b/core/designsystem/src/main/res/drawable/ic_setting.xml new file mode 100644 index 0000000..2a6fafe --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_setting.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index c9eade1..f2c3186 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -47,4 +47,26 @@ 토닥이 행운 액션 마이 + + + My page + 사주 정보 관리 + 알림 설정 + 앱 설정 + 1:1 문의 + 로그아웃 + 앱 버전 + v %1$s + 만세력 보기 + 수정 + (최신 버전) + + + · + 남성 + 여성 + 양력 + 음력 + %1$d시 %2$02d분 + -%1$s, %2$s diff --git a/core/domain/src/main/java/com/kikidan/domain/model/user/User.kt b/core/domain/src/main/java/com/kikidan/domain/model/user/User.kt index 8588949..760f193 100644 --- a/core/domain/src/main/java/com/kikidan/domain/model/user/User.kt +++ b/core/domain/src/main/java/com/kikidan/domain/model/user/User.kt @@ -5,6 +5,7 @@ import java.time.LocalTime data class User( val id: String, + val name: String, val gender: Gender, val birth: Birth, ) diff --git a/feature/mypage/.gitignore b/feature/mypage/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/feature/mypage/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/mypage/build.gradle.kts b/feature/mypage/build.gradle.kts new file mode 100644 index 0000000..95da1d6 --- /dev/null +++ b/feature/mypage/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.todakun.feature) +} + +android { + namespace = "com.kikidan.mypage" +} + +dependencies { + implementation(libs.play.app.update.ktx) + implementation(libs.kotlinx.coroutines.play.services) +} diff --git a/feature/mypage/consumer-rules.pro b/feature/mypage/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/feature/mypage/proguard-rules.pro b/feature/mypage/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/feature/mypage/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/mypage/src/main/AndroidManifest.xml b/feature/mypage/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8bdb7e1 --- /dev/null +++ b/feature/mypage/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/AppVersionChecker.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/AppVersionChecker.kt new file mode 100644 index 0000000..db56857 --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/AppVersionChecker.kt @@ -0,0 +1,30 @@ +package com.kikidan.mypage.home + +import android.content.Context +import com.google.android.play.core.appupdate.AppUpdateManagerFactory +import com.google.android.play.core.install.model.UpdateAvailability +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.tasks.await +import javax.inject.Inject + +class AppVersionChecker + @Inject + constructor( + @ApplicationContext private val context: Context, + ) { + fun getCurrentVersionName(): String = + context.packageManager + .getPackageInfo(context.packageName, 0) + .versionName + .orEmpty() + + suspend fun isLatestVersion(): Boolean = + try { + val appUpdateManager = AppUpdateManagerFactory.create(context) + val appUpdateInfo = appUpdateManager.appUpdateInfo.await() + appUpdateInfo.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE + } catch (e: Exception) { + // Play Store가 없는 기기(에뮬레이터 등)에서도 마이페이지 조회 자체는 실패하면 안 되므로 조회 실패 시 최신 버전으로 간주한다. + true + } + } diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/MyPageHomeViewModel.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/MyPageHomeViewModel.kt new file mode 100644 index 0000000..78e9cc5 --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/MyPageHomeViewModel.kt @@ -0,0 +1,48 @@ +package com.kikidan.mypage.home + +import androidx.lifecycle.ViewModel +import com.kikidan.domain.usecase.mypage.GetMyPageInfoUseCase +import com.kikidan.mypage.home.model.MyPageHomeSideEffect +import com.kikidan.mypage.home.model.MyPageHomeUiModel +import com.kikidan.mypage.home.model.MyPageHomeUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class MyPageHomeViewModel + @Inject + constructor( + private val getMyPageInfoUseCase: GetMyPageInfoUseCase, + private val appVersionChecker: AppVersionChecker, + ) : ViewModel(), + ContainerHost { + override val container: Container = + container(MyPageHomeUiState.Loading) { + repeatOnSubscription { + loadMyPageInfo() + } + } + + fun loadMyPageInfo() = + intent { + getMyPageInfoUseCase() + .onSuccess { info -> + val isLatestVersion = appVersionChecker.isLatestVersion() + reduce { + MyPageHomeUiState.Success( + MyPageHomeUiModel( + user = info.user, + sajuPalja = info.sajuPalja, + appVersionName = appVersionChecker.getCurrentVersionName(), + isLatestVersion = isLatestVersion, + ), + ) + } + }.onFailure { throwable -> + reduce { MyPageHomeUiState.Fail(throwable) } + } + } + } diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeSideEffect.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeSideEffect.kt new file mode 100644 index 0000000..66236d5 --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeSideEffect.kt @@ -0,0 +1,3 @@ +package com.kikidan.mypage.home.model + +sealed interface MyPageHomeSideEffect diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeUiModel.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeUiModel.kt new file mode 100644 index 0000000..1c1563f --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeUiModel.kt @@ -0,0 +1,11 @@ +package com.kikidan.mypage.home.model + +import com.kikidan.domain.model.saju.SajuPalja +import com.kikidan.domain.model.user.User + +data class MyPageHomeUiModel( + val user: User, + val sajuPalja: SajuPalja, + val appVersionName: String, + val isLatestVersion: Boolean = true, +) diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeUiState.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeUiState.kt new file mode 100644 index 0000000..f5f510e --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageHomeUiState.kt @@ -0,0 +1,13 @@ +package com.kikidan.mypage.home.model + +sealed interface MyPageHomeUiState { + data object Loading : MyPageHomeUiState + + data class Success( + val model: MyPageHomeUiModel, + ) : MyPageHomeUiState + + data class Fail( + val e: Throwable, + ) : MyPageHomeUiState +} diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageMenuType.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageMenuType.kt new file mode 100644 index 0000000..90c7535 --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/model/MyPageMenuType.kt @@ -0,0 +1,27 @@ +package com.kikidan.mypage.home.model + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.ui.graphics.Color +import com.kikidan.designsystem.R +import com.kikidan.designsystem.theme.TodakunColor + +enum class MyPageMenuType( + @param:DrawableRes val iconRes: Int, + @param:StringRes val labelRes: Int, + val showChevron: Boolean = true, + val iconTint: Color = TodakunColor.gray975, + val textColor: Color = TodakunColor.black, +) { + SAJU_INFO(iconRes = R.drawable.ic_manage_saju_info, labelRes = R.string.mypage_menu_saju_info), + NOTIFICATION_SETTING(iconRes = R.drawable.ic_bell, labelRes = R.string.mypage_menu_notification_setting), + APP_SETTING(iconRes = R.drawable.ic_setting, labelRes = R.string.mypage_menu_app_setting), + INQUIRY(iconRes = R.drawable.ic_mail, labelRes = R.string.mypage_menu_inquiry), + LOGOUT( + iconRes = R.drawable.ic_logout, + labelRes = R.string.mypage_menu_logout, + showChevron = false, + iconTint = TodakunColor.gray500, + textColor = TodakunColor.gray600, + ), +} diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/MyPageHomeRoute.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/MyPageHomeRoute.kt new file mode 100644 index 0000000..c3f9ada --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/MyPageHomeRoute.kt @@ -0,0 +1,21 @@ +package com.kikidan.mypage.home.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import com.kikidan.mypage.home.MyPageHomeViewModel +import org.orbitmvi.orbit.compose.collectAsState + +@Composable +fun MyPageHomeRoute( + modifier: Modifier = Modifier, + viewModel: MyPageHomeViewModel = hiltViewModel(), +) { + val uiState by viewModel.collectAsState() + + MyPageHomeScreen( + uiState = uiState, + modifier = modifier, + ) +} diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/MyPageHomeScreen.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/MyPageHomeScreen.kt new file mode 100644 index 0000000..693196d --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/MyPageHomeScreen.kt @@ -0,0 +1,190 @@ +package com.kikidan.mypage.home.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.kikidan.designsystem.R +import com.kikidan.designsystem.component.TodakunDivider +import com.kikidan.designsystem.component.TodakunDividerType +import com.kikidan.designsystem.theme.TodakunColor +import com.kikidan.designsystem.theme.TodakunTheme +import com.kikidan.designsystem.theme.TodakunTypography +import com.kikidan.domain.model.saju.CheonGan +import com.kikidan.domain.model.saju.JiJi +import com.kikidan.domain.model.saju.SajuPalja +import com.kikidan.domain.model.saju.SajuPillar +import com.kikidan.domain.model.user.Birth +import com.kikidan.domain.model.user.BirthTime +import com.kikidan.domain.model.user.DateType +import com.kikidan.domain.model.user.Gender +import com.kikidan.domain.model.user.User +import com.kikidan.mypage.home.model.MyPageHomeUiModel +import com.kikidan.mypage.home.model.MyPageHomeUiState +import com.kikidan.mypage.home.model.MyPageMenuType +import com.kikidan.mypage.home.ui.component.MyPageHomeHeader +import com.kikidan.mypage.home.ui.component.MyPageMenuItem +import com.kikidan.mypage.home.ui.component.ProfileCard +import java.time.LocalDate + +@Composable +fun MyPageHomeScreen( + uiState: MyPageHomeUiState, + modifier: Modifier = Modifier, + onEditClick: () -> Unit = {}, + onViewMansaeryeokClick: () -> Unit = {}, + onMenuItemClick: (MyPageMenuType) -> Unit = {}, +) { + Column( + modifier = + modifier + .fillMaxSize() + .background(TodakunColor.white), + ) { + MyPageHomeHeader(modifier = Modifier.fillMaxWidth()) + + when (uiState) { + is MyPageHomeUiState.Loading -> { + MyPageHomeLoading(modifier = Modifier.weight(1f)) + } + + is MyPageHomeUiState.Fail -> { + MyPageHomeError(modifier = Modifier.weight(1f)) + } + + is MyPageHomeUiState.Success -> { + MyPageHomeContent( + model = uiState.model, + onEditClick = onEditClick, + onViewMansaeryeokClick = onViewMansaeryeokClick, + onMenuItemClick = onMenuItemClick, + modifier = Modifier.weight(1f), + ) + } + } + } +} + +@Composable +private fun MyPageHomeLoading(modifier: Modifier = Modifier) { + // 로딩 요구사항 없음 + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + } +} + +@Composable +private fun MyPageHomeError(modifier: Modifier = Modifier) { + // 에러 요구사항 없음 + Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + } +} + +@Composable +private fun MyPageHomeContent( + model: MyPageHomeUiModel, + onEditClick: () -> Unit, + onViewMansaeryeokClick: () -> Unit, + onMenuItemClick: (MyPageMenuType) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.fillMaxSize()) { + ProfileCard( + user = model.user, + sajuPalja = model.sajuPalja, + onEditClick = onEditClick, + onViewMansaeryeokClick = onViewMansaeryeokClick, + modifier = Modifier.padding(horizontal = 20.dp), + ) + + Spacer(modifier = Modifier.height(24.dp)) + TodakunDivider(type = TodakunDividerType.Section) + + MyPageMenuType.entries.forEach { menuType -> + MyPageMenuItem( + painter = painterResource(menuType.iconRes), + label = stringResource(menuType.labelRes), + onClick = { onMenuItemClick(menuType) }, + showChevron = menuType.showChevron, + iconTint = menuType.iconTint, + textColor = menuType.textColor, + modifier = Modifier.padding(horizontal = 20.dp), + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 20.dp), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(R.string.mypage_app_version_label), + style = TodakunTypography.body3Medium, + color = TodakunColor.gray400, + ) + Spacer(modifier = Modifier.width(16.dp)) + val versionText = + stringResource(R.string.mypage_app_version_format, model.appVersionName) + + if (model.isLatestVersion) " " + stringResource(R.string.mypage_latest_version) else "" + Text( + text = versionText, + style = TodakunTypography.body3Medium, + color = TodakunColor.gray400, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun MyPageHomeScreenPreview() { + TodakunTheme { + MyPageHomeScreen( + uiState = + MyPageHomeUiState.Success( + MyPageHomeUiModel( + user = + User( + id = "preview", + name = "토닥이", + gender = Gender.FEMALE, + birth = + Birth( + dateType = DateType.SOLAR, + date = LocalDate.of(1999, 2, 13), + time = BirthTime.SIN, + ), + ), + sajuPalja = + SajuPalja( + yearPillar = SajuPillar(CheonGan.SIN, JiJi.MI), + monthPillar = SajuPillar(CheonGan.GI, JiJi.SA), + dayPillar = SajuPillar(CheonGan.GYE, JiJi.MYO), + hourPillar = SajuPillar(CheonGan.JEONG, JiJi.CHUK), + ), + appVersionName = "1.0.0", + isLatestVersion = false, + ), + ), + ) + } +} diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/MyPageHomeHeader.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/MyPageHomeHeader.kt new file mode 100644 index 0000000..6250dbe --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/MyPageHomeHeader.kt @@ -0,0 +1,21 @@ +package com.kikidan.mypage.home.ui.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.kikidan.designsystem.R +import com.kikidan.designsystem.theme.TodakunColor +import com.kikidan.designsystem.theme.TodakunTypography + +@Composable +internal fun MyPageHomeHeader(modifier: Modifier = Modifier) { + Text( + text = stringResource(R.string.mypage_home_title), + style = TodakunTypography.heading4Bold, + color = TodakunColor.black, + modifier = modifier.padding(horizontal = 20.dp, vertical = 16.dp), + ) +} diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/MyPageMenuItem.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/MyPageMenuItem.kt new file mode 100644 index 0000000..b8b4ec4 --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/MyPageMenuItem.kt @@ -0,0 +1,62 @@ +package com.kikidan.mypage.home.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.kikidan.designsystem.R +import com.kikidan.designsystem.theme.TodakunColor +import com.kikidan.designsystem.theme.TodakunTypography + +@Composable +internal fun MyPageMenuItem( + painter: Painter, + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + iconTint: Color = TodakunColor.gray975, + textColor: Color = TodakunColor.black, + showChevron: Boolean = true, +) { + Row( + modifier = + modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painter, + contentDescription = null, + tint = iconTint, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = label, + style = TodakunTypography.body2Medium, + color = textColor, + modifier = Modifier.weight(1f), + ) + if (showChevron) { + Icon( + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = null, + tint = TodakunColor.gray400, + ) + } + } +} diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/ProfileCard.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/ProfileCard.kt new file mode 100644 index 0000000..87ba42b --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/ProfileCard.kt @@ -0,0 +1,213 @@ +package com.kikidan.mypage.home.ui.component + +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.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.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.kikidan.designsystem.R +import com.kikidan.designsystem.component.button.PrimaryButton +import com.kikidan.designsystem.component.button.TodakunButtonSize +import com.kikidan.designsystem.theme.TodakunColor +import com.kikidan.designsystem.theme.TodakunTheme +import com.kikidan.designsystem.theme.TodakunTypography +import com.kikidan.domain.model.saju.CheonGan +import com.kikidan.domain.model.saju.JiJi +import com.kikidan.domain.model.saju.SajuPalja +import com.kikidan.domain.model.saju.SajuPillar +import com.kikidan.domain.model.user.Birth +import com.kikidan.domain.model.user.BirthTime +import com.kikidan.domain.model.user.DateType +import com.kikidan.domain.model.user.Gender +import com.kikidan.domain.model.user.User +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +@Composable +internal fun ProfileCard( + user: User, + sajuPalja: SajuPalja, + onEditClick: () -> Unit, + onViewMansaeryeokClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(TodakunColor.white) + .border(1.dp, TodakunColor.gray100, RoundedCornerShape(20.dp)) + .padding(20.dp), + ) { + Row(verticalAlignment = Alignment.Top) { + Column(modifier = Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = user.name, + style = TodakunTypography.heading3Bold, + color = TodakunColor.gray975, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringResource(R.string.separator_dot), + style = TodakunTypography.body2Regular, + color = TodakunColor.gray700, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = user.gender.displayName(), + style = TodakunTypography.body2Regular, + color = TodakunColor.gray700, + ) + } + Spacer(modifier = Modifier.height(4.dp)) + UserBirthDateTime(birth = user.birth) + } + EditButton(onClick = onEditClick) + } + + Spacer(modifier = Modifier.height(20.dp)) + SajuPaljaGrid(sajuPalja = sajuPalja) + Spacer(modifier = Modifier.height(20.dp)) + + PrimaryButton( + text = stringResource(R.string.mypage_view_mansaeryeok_button), + onClick = onViewMansaeryeokClick, + size = TodakunButtonSize.Medium, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun UserBirthDateTime(birth: Birth) { + val dateText = "${birth.date.format(BirthDateFormatter)} ${birth.dateType.displayName()}" + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = dateText, + style = TodakunTypography.body3Medium, + color = TodakunColor.gray700, + ) + if (birth.time != BirthTime.UNKNOWN) { + Text( + text = stringResource(R.string.separator_dot), + style = TodakunTypography.body3Medium, + color = TodakunColor.gray700, + modifier = Modifier.padding(horizontal = 8.dp), + ) + Text( + text = birth.time.displayText(), + style = TodakunTypography.body3Medium, + color = TodakunColor.gray700, + ) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun UserBirthDateTimePreview() { + val birth = + Birth( + dateType = DateType.SOLAR, + date = LocalDate.of(1999, 2, 13), + time = BirthTime.SIN, + ) + UserBirthDateTime( + birth = birth, + ) +} + +@Composable +private fun EditButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = + modifier + .clip(RoundedCornerShape(8.dp)) + .border( + 1.dp, + TodakunColor.primary500, + RoundedCornerShape(8.dp), + ).clickable(onClick = onClick) + .background(TodakunColor.white) + .padding(horizontal = 19.dp, vertical = 8.dp), + ) { + Text( + text = stringResource(R.string.mypage_edit_button), + style = TodakunTypography.caption1SemiBold, + color = TodakunColor.primary700, + ) + } +} + +private val BirthDateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") + +@Composable +private fun Gender.displayName(): String = + when (this) { + Gender.MALE -> stringResource(R.string.gender_male) + Gender.FEMALE -> stringResource(R.string.gender_female) + } + +@Composable +private fun DateType.displayName(): String = + when (this) { + DateType.SOLAR -> stringResource(R.string.date_type_solar) + DateType.LUNAR -> stringResource(R.string.date_type_lunar) + } + +@Composable +private fun BirthTime.displayText(): String { + val start = startTime ?: return "" + return stringResource(R.string.birth_time_format, start.hour, start.minute) +} + +@Preview(showBackground = true) +@Composable +private fun ProfileCardPreview() { + TodakunTheme { + ProfileCard( + user = + User( + id = "preview", + name = "토닥이", + gender = Gender.FEMALE, + birth = + Birth( + dateType = DateType.SOLAR, + date = LocalDate.of(1999, 2, 13), + time = BirthTime.SIN, + ), + ), + sajuPalja = + SajuPalja( + yearPillar = SajuPillar(CheonGan.SIN, JiJi.MI), + monthPillar = SajuPillar(CheonGan.GI, JiJi.SA), + dayPillar = SajuPillar(CheonGan.GYE, JiJi.MYO), + hourPillar = SajuPillar(CheonGan.JEONG, JiJi.CHUK), + ), + onEditClick = {}, + onViewMansaeryeokClick = {}, + ) + } +} diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/SajuCard.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/SajuCard.kt new file mode 100644 index 0000000..1c88575 --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/SajuCard.kt @@ -0,0 +1,77 @@ +package com.kikidan.mypage.home.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.kikidan.designsystem.R +import com.kikidan.designsystem.theme.TodakunColor +import com.kikidan.designsystem.theme.TodakunTheme +import com.kikidan.designsystem.theme.TodakunTypography +import com.kikidan.domain.model.saju.Ohaeng + +@Composable +internal fun SajuCard( + hanja: String, + reading: String, + ohaeng: Ohaeng, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .aspectRatio(1f) + .clip(RoundedCornerShape(12.dp)) + .background(ohaeng.containerColor()) + .padding(horizontal = 6.dp, vertical = 9.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = hanja, + style = TodakunTypography.body1Bold, + color = TodakunColor.gray975, + ) + Text( + text = stringResource(R.string.saju_card_label_format, reading, ohaeng.hanja), + style = TodakunTypography.caption3Regular, + color = TodakunColor.gray975, + ) + } +} + +private fun Ohaeng.containerColor(): Color = + when (this) { + Ohaeng.MOK -> TodakunColor.teal200 + Ohaeng.HWA -> TodakunColor.red200 + Ohaeng.TO -> TodakunColor.orange200 + Ohaeng.GEUM -> TodakunColor.coolGray300 + Ohaeng.SU -> TodakunColor.sky200 + } + +@Preview(showBackground = true) +@Composable +private fun SajuCardPreview() { + TodakunTheme { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + SajuCard(hanja = "辛", reading = "신", ohaeng = Ohaeng.GEUM, modifier = Modifier.width(72.dp)) + SajuCard(hanja = "己", reading = "기", ohaeng = Ohaeng.TO, modifier = Modifier.width(72.dp)) + SajuCard(hanja = "癸", reading = "계", ohaeng = Ohaeng.SU, modifier = Modifier.width(72.dp)) + SajuCard(hanja = "丁", reading = "정", ohaeng = Ohaeng.HWA, modifier = Modifier.width(72.dp)) + SajuCard(hanja = "卯", reading = "묘", ohaeng = Ohaeng.MOK, modifier = Modifier.width(72.dp)) + } + } +} diff --git a/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/SajuPaljaGrid.kt b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/SajuPaljaGrid.kt new file mode 100644 index 0000000..55cd720 --- /dev/null +++ b/feature/mypage/src/main/java/com/kikidan/mypage/home/ui/component/SajuPaljaGrid.kt @@ -0,0 +1,76 @@ +package com.kikidan.mypage.home.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.kikidan.designsystem.theme.TodakunColor +import com.kikidan.designsystem.theme.TodakunTheme +import com.kikidan.domain.model.saju.CheonGan +import com.kikidan.domain.model.saju.JiJi +import com.kikidan.domain.model.saju.SajuPalja +import com.kikidan.domain.model.saju.SajuPillar + +@Composable +internal fun SajuPaljaGrid( + sajuPalja: SajuPalja, + modifier: Modifier = Modifier, +) { + val pillars: List = + listOfNotNull(sajuPalja.yearPillar, sajuPalja.monthPillar, sajuPalja.dayPillar, sajuPalja.hourPillar) + + Column( + modifier = + modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(TodakunColor.coolGray50) + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + pillars.forEach { pillar -> + SajuCard( + hanja = pillar.cheonGan.hanja, + reading = pillar.cheonGan.displayName, + ohaeng = pillar.cheonGan.ohaeng, + modifier = Modifier.weight(1f), + ) + } + } + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + pillars.forEach { pillar -> + SajuCard( + hanja = pillar.jiJi.hanja, + reading = pillar.jiJi.displayName, + ohaeng = pillar.jiJi.ohaeng, + modifier = Modifier.weight(1f), + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SajuPaljaGridPreview() { + TodakunTheme { + SajuPaljaGrid( + sajuPalja = + SajuPalja( + yearPillar = SajuPillar(CheonGan.SIN, JiJi.MI), + monthPillar = SajuPillar(CheonGan.GI, JiJi.SA), + dayPillar = SajuPillar(CheonGan.GYE, JiJi.MYO), + hourPillar = SajuPillar(CheonGan.JEONG, JiJi.CHUK), + ), + ) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81712ff..39989b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,7 @@ material3AdaptiveNav3 = "1.3.0-rc01" ktlint = "1.8.0" ktlintGradlePlugin = "14.2.0" composeRules = "0.6.2" +playAppUpdate = "2.1.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -68,6 +69,8 @@ orbit-test = { group = "org.orbit-mvi", name = "orbit-test", version.ref = "orbi # Navigation 3 libraries kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-play-services = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-play-services", version.ref = "kotlinx-coroutines" } +play-app-update-ktx = { group = "com.google.android.play", name = "app-update-ktx", version.ref = "playAppUpdate" } androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" } @@ -85,4 +88,5 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlintGradlePlugin" } todakun-android-signing = { id = "todakun.android.signing" } +todakun-feature = { id = "todakun.feature" } diff --git a/settings.gradle.kts b/settings.gradle.kts index c4176d5..48a0d32 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -36,3 +36,4 @@ include(":core:data-remote") include(":core:navigation") include(":feature:login") include(":core:designsystem") +include(":feature:mypage")