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")