diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 03038856..94e05c1f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,7 +11,6 @@ dependencies { implementation(project(":feature:home")) implementation(project(":feature:create")) implementation(project(":feature:profile")) - implementation(project(":feature:setting")) implementation(project(":feature:detail")) implementation(project(":feature:friend")) implementation(project(":feature:management")) diff --git a/app/src/main/java/com/idiotfrogs/memoryseal/MainActivity.kt b/app/src/main/java/com/idiotfrogs/memoryseal/MainActivity.kt index 625da32c..1513dfee 100644 --- a/app/src/main/java/com/idiotfrogs/memoryseal/MainActivity.kt +++ b/app/src/main/java/com/idiotfrogs/memoryseal/MainActivity.kt @@ -38,7 +38,6 @@ import com.idiotfrogs.navigation.Routes import com.idiotfrogs.preview.PreviewRoute import com.idiotfrogs.profile.editprofile.EditProfileRoute import com.idiotfrogs.profile.profile.ProfileRoute -import com.idiotfrogs.setting.SettingRoute import com.idiotfrogs.splash.SplashRoute import dagger.hilt.android.AndroidEntryPoint @@ -136,7 +135,6 @@ class MainActivity : ComponentActivity() { entry { CreateRoute() } entry { ProfileRoute() } entry { EditProfileRoute() } - entry { SettingRoute() } entry { DetailRoute(capsuleId = it.id) } entry { MessageRoute(capsuleId = it.id) } entry { PreviewRoute(capsuleId = it.id) } diff --git a/core/designsystem/src/main/java/com/idiotfrogs/designsystem/util/WavyStroke.kt b/core/designsystem/src/main/java/com/idiotfrogs/designsystem/util/WavyStroke.kt index feb8b90a..859fe3ca 100644 --- a/core/designsystem/src/main/java/com/idiotfrogs/designsystem/util/WavyStroke.kt +++ b/core/designsystem/src/main/java/com/idiotfrogs/designsystem/util/WavyStroke.kt @@ -22,7 +22,7 @@ import kotlin.math.min import kotlin.math.roundToInt import kotlin.math.sin -enum class DrawType { TOP, BOTTOM, START, END, ALL } +enum class DrawType { TOP, BOTTOM, START, END, ALL, TOP_SIDES } enum class WavyAlign { INNER, OUTER } fun Modifier.wavyStroke( @@ -102,6 +102,16 @@ fun Modifier.wavyStroke( seed = seed + size.width.roundToInt() + size.height.roundToInt() * 1_000_003L, ) } + DrawType.TOP_SIDES -> { + makeTopSidesWavyPath( + rect = rect, + cornerRadius = radius, + spacing = spacingPx, + amplitude = ampPx, + seed = seed + size.width.roundToInt() + + size.height.roundToInt() * 1_000_003L, + ) + } } val fillPath = if (drawType == DrawType.ALL) { @@ -130,6 +140,10 @@ fun Modifier.wavyStroke( lineTo(0f, 0f) lineTo(size.width, 0f) } + DrawType.TOP_SIDES -> { + lineTo(rect.right, size.height) + lineTo(rect.left, size.height) + } } close() } @@ -366,5 +380,89 @@ private fun makeSingleAxisWavyPath( } } +private fun makeTopSidesWavyPath( + rect: Rect, + cornerRadius: Float, + spacing: Float, + amplitude: Float, + seed: Long, +): Path { + val wavyData = mutableListOf() + + fun line(from: Offset, to: Offset, normal: Offset) { + val dx = to.x - from.x + val dy = to.y - from.y + val length = hypot(dx, dy) + val count = max(1, (length / spacing).toInt()) + + repeat(count) { i -> + val t = i / count.toFloat() + wavyData += WavyData( + point = Offset(from.x + dx * t, from.y + dy * t), + normal = normal, + ) + } + } + + fun arc(center: Offset, radius: Float, start: Float, end: Float) { + val count = max(1, (abs(end - start) * radius / spacing).toInt()) + + repeat(count) { i -> + val t = i / count.toFloat() + val angle = start + (end - start) * t + wavyData += WavyData( + point = Offset( + center.x + cos(angle) * radius, + center.y + sin(angle) * radius, + ), + normal = Offset(cos(angle), sin(angle)), + ) + } + } + + line( + Offset(rect.left, rect.bottom - cornerRadius), + Offset(rect.left, rect.top + cornerRadius), + Offset(-1f, 0f) + ) + arc( + Offset(rect.left + cornerRadius, rect.top + cornerRadius), + cornerRadius, PI.toFloat(), PI.toFloat() * 1.5f + ) + line( + Offset(rect.left + cornerRadius, rect.top), + Offset(rect.right - cornerRadius, rect.top), + Offset(0f, -1f) + ) + arc( + Offset(rect.right - cornerRadius, rect.top + cornerRadius), + cornerRadius, -PI.toFloat() / 2f, 0f + ) + line( + Offset(rect.right, rect.top + cornerRadius), + Offset(rect.right, rect.bottom - cornerRadius), + Offset(1f, 0f) + ) + + var nextSeed = seed + val points = wavyData.map { + nextSeed = nextSeed * 6364136223846793005L + 1442695040888963407L + val random = ((nextSeed ushr 1) % 2000L) / 1000f - 1f + val offset = random * amplitude + it.point + it.normal * offset + } + + return Path().apply { + if (points.isEmpty()) return@apply + + moveTo(points.first().x, points.first().y) + points.zipWithNext { current, next -> + val mid = midpoint(current, next) + quadraticTo(current.x, current.y, mid.x, mid.y) + } + lineTo(points.last().x, points.last().y) + } +} + private fun midpoint(a: Offset, b: Offset): Offset = Offset((a.x + b.x) / 2f, (a.y + b.y) / 2f) \ No newline at end of file diff --git a/core/navigation/src/main/java/com/idiotfrogs/navigation/Routes.kt b/core/navigation/src/main/java/com/idiotfrogs/navigation/Routes.kt index 28e6cc19..810ad2b3 100644 --- a/core/navigation/src/main/java/com/idiotfrogs/navigation/Routes.kt +++ b/core/navigation/src/main/java/com/idiotfrogs/navigation/Routes.kt @@ -19,8 +19,6 @@ sealed interface Routes: NavKey { @Serializable data object EditProfile : Routes @Serializable - data object Setting : Routes - @Serializable data class Friend(val id: Long) : Routes @Serializable data class Detail(val id: Long) : Routes diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileHeader.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileHeader.kt index 8b2c81b2..6812f83b 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileHeader.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileHeader.kt @@ -2,8 +2,7 @@ package com.idiotfrogs.profile.component import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -23,15 +22,13 @@ import com.idiotfrogs.resource.R fun ProfileHeader( modifier: Modifier = Modifier, onBack: () -> Unit, - onSetting: () -> Unit ) { - Row( + Box( modifier = modifier .fillMaxWidth() .background(MSTheme.color.white) .padding(horizontal = 20.dp, vertical = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + contentAlignment = Alignment.CenterStart, ) { Image( modifier = Modifier @@ -41,18 +38,12 @@ fun ProfileHeader( contentDescription = "chevron_left", ) MSText( + modifier = Modifier.align(Alignment.Center), text = "프로필", fontWeight = FontWeight.Bold, fontSize = 20.dp, color = MSTheme.color.greyG5 ) - Image( - modifier = Modifier - .size(24.dp) - .noRippleClickable(onClick = onSetting), - painter = painterResource(R.drawable.ic_setting), - contentDescription = "설정" - ) } } @@ -61,6 +52,5 @@ fun ProfileHeader( private fun ProfileHeaderPreview() { ProfileHeader( onBack = {}, - onSetting = {} ) } diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileScreen.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileScreen.kt index d9f59c9a..410aa414 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileScreen.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileScreen.kt @@ -1,31 +1,42 @@ package com.idiotfrogs.profile.profile +import android.content.pm.PackageManager +import android.os.Build +import androidx.compose.foundation.Image 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.size import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyGridItemScope -import androidx.compose.foundation.lazy.grid.LazyGridScope -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.idiotfrogs.designsystem.component.MSLoadingOverlay -import com.idiotfrogs.designsystem.component.MSDashHorizontalDivider import com.idiotfrogs.designsystem.component.MSText +import com.idiotfrogs.designsystem.component.MSTitleDialog import com.idiotfrogs.designsystem.theme.MSTheme -import com.idiotfrogs.extension.toYearMonthDay +import com.idiotfrogs.designsystem.util.DrawType +import com.idiotfrogs.designsystem.util.noRippleClickable +import com.idiotfrogs.designsystem.util.wavyStroke import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleRole import com.idiotfrogs.model.timecapsule.TimeCapsuleStatus @@ -34,7 +45,7 @@ import com.idiotfrogs.navigation.LocalComposeMSNavigator import com.idiotfrogs.navigation.Routes import com.idiotfrogs.profile.component.ProfileCard import com.idiotfrogs.profile.component.ProfileHeader -import com.idiotfrogs.profile.component.ProfileTicketCard +import com.idiotfrogs.resource.R import kotlinx.datetime.TimeZone import kotlinx.datetime.todayIn import org.orbitmvi.orbit.compose.collectAsState @@ -52,9 +63,12 @@ fun ProfileRoute( viewModel.collectSideEffect { event -> when (event) { + ProfileSideEffect.NavigateToLogin -> { + navigator.clear() + navigator.navigate(Routes.Login) + } ProfileSideEffect.NavigateToBack -> navigator.popBackStack() ProfileSideEffect.NavigateToEditProfile -> navigator.navigate(Routes.EditProfile) - ProfileSideEffect.NavigateToSetting -> navigator.navigate(Routes.Setting) is ProfileSideEffect.NavigateToDetail -> navigator.navigate(Routes.Detail(event.id)) } } @@ -76,6 +90,55 @@ fun ProfileScreen( data: ProfileData, onAction: (ProfileAction) -> Unit ) { + var showLogoutDialog by remember { mutableStateOf(false) } + var showWithdrawDialog by remember { mutableStateOf(false) } + + if (showLogoutDialog) { + MSTitleDialog( + title = "로그아웃", + confirmText = "로그아웃", + cancelText = "유지", + onConfirm = { + showLogoutDialog = false + onAction.invoke(ProfileAction.LogoutConfirmed) + }, + onCancel = { showLogoutDialog = false }, + content = { + Spacer(modifier = Modifier.height(8.dp)) + MSText( + text = "메실에서 로그아웃 하시겠습니까?", + fontWeight = FontWeight.Normal, + fontSize = 16.dp, + color = MSTheme.color.greyG5 + ) + Spacer(modifier = Modifier.height(24.dp)) + } + ) + } + + if (showWithdrawDialog) { + MSTitleDialog( + title = "회원탈퇴", + confirmText = "탈퇴", + cancelText = "취소", + onConfirm = { + showWithdrawDialog = false + onAction.invoke(ProfileAction.WithdrawConfirmed) + }, + onCancel = { showWithdrawDialog = false }, + content = { + Spacer(modifier = Modifier.height(8.dp)) + MSText( + text = "메실 회원을 탈퇴하시겠습니까?\n티켓에 저장된 내용은 삭제되지 않습니다.", + fontWeight = FontWeight.Normal, + fontSize = 16.dp, + color = MSTheme.color.greyG5 + ) + Spacer(modifier = Modifier.height(24.dp)) + } + ) + } + Box( modifier = Modifier .fillMaxSize() @@ -84,61 +147,135 @@ fun ProfileScreen( ProfileHeader( modifier = Modifier.zIndex(1f), onBack = { onAction(ProfileAction.BackClicked) }, - onSetting = { onAction(ProfileAction.SettingClicked) } ) - LazyVerticalGrid( + Column( modifier = Modifier + .background(Color.White) .fillMaxSize() - .background(MSTheme.color.white) .padding(horizontal = 20.dp), - columns = GridCells.Fixed(2), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + horizontalAlignment = Alignment.CenterHorizontally ) { - maxLineItem { - ProfileCard( - modifier = Modifier.padding(top = (HeaderHeight + 24).dp), - nickname = data.user?.nickname ?: "", - imageUrl = data.user?.profileImageUrl?.ifEmpty { null }, - onEditClick = { onAction(ProfileAction.EditProfileClicked) } - ) - } - maxLineItem { - MSText( - modifier = Modifier.padding(top = 16.dp), - text = "오픈된 티켓", - fontWeight = FontWeight.Bold, - fontSize = 16.dp, - color = MSTheme.color.greyG5, - textAlign = TextAlign.Center - ) - } - maxLineItem { - MSDashHorizontalDivider( - thickness = 2.dp, - dashWidth = 10.dp, - gapWidth = 10.dp - ) - } - items(data.capsules) { - ProfileTicketCard( - imageUrl = it.mainImageUrl, - title = it.title, - date = it.createdAt.toYearMonthDay(), - onClick = { onAction.invoke(ProfileAction.TicketClicked(it.timeCapsuleId))} - ) + ProfileCard( + modifier = Modifier.padding(top = (HeaderHeight + 24).dp), + nickname = data.user?.nickname ?: "", + imageUrl = data.user?.profileImageUrl?.ifEmpty { null }, + onEditClick = { onAction(ProfileAction.EditProfileClicked) } + ) + Spacer(modifier = Modifier.height(44.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .wavyStroke( + drawType = DrawType.TOP_SIDES, + color = MSTheme.color.bgNormal, + fillColor = MSTheme.color.bgNormal + ) + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + val versionName = rememberAppVersion() + MSText( + text = "앱 버전", + fontSize = 16.dp, + fontWeight = FontWeight.Medium, + color = MSTheme.color.greyG5 + ) + MSText( + text = "v$versionName", + fontSize = 16.dp, + fontWeight = FontWeight.Normal, + color = MSTheme.color.greyG4 + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + MSText( + text = "이용 약관", + fontSize = 16.dp, + fontWeight = FontWeight.Medium, + color = MSTheme.color.greyG5 + ) + Image( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = "arrow_right" + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .noRippleClickable { showLogoutDialog = true }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + MSText( + text = "로그아웃", + fontSize = 16.dp, + fontWeight = FontWeight.Medium, + color = MSTheme.color.greyG5 + ) + Image( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = "arrow_right" + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .noRippleClickable { showWithdrawDialog = true }, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + MSText( + text = "회원탈퇴", + fontSize = 16.dp, + fontWeight = FontWeight.Medium, + color = MSTheme.color.greyG5 + ) + Image( + modifier = Modifier.size(16.dp), + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = "arrow_right" + ) + } } } } } -private fun LazyGridScope.maxLineItem( - content: @Composable LazyGridItemScope.() -> Unit -) { - item( - span = { GridItemSpan(maxLineSpan) }, - content = content - ) +@Composable +fun rememberAppVersion(): String { + val context = LocalContext.current + return remember { + runCatching { + val packageName = context.packageName + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.getPackageInfo( + packageName, PackageManager.PackageInfoFlags.of(0) + ).versionName ?: "" + } else { + context.packageManager.getPackageInfo(packageName, 0).versionName ?: "" + } + }.getOrDefault("") + } } @Preview diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileViewModel.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileViewModel.kt index 1fa37d00..8f94de9e 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileViewModel.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileViewModel.kt @@ -1,8 +1,10 @@ package com.idiotfrogs.profile.profile import androidx.compose.runtime.Immutable +import com.idiotfrogs.domain.usecase.auth.LogoutUseCase import com.idiotfrogs.domain.usecase.timecapsule.GetMyTimeCapsuleUseCase import com.idiotfrogs.domain.usecase.user.GetMyProfileUseCase +import com.idiotfrogs.domain.usecase.user.WithdrawUseCase import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleStatus import com.idiotfrogs.model.user.ProfileResponse @@ -19,7 +21,9 @@ import javax.inject.Inject @HiltViewModel class ProfileViewModel @Inject constructor( private val getMyTimeCapsuleUseCase: GetMyTimeCapsuleUseCase, - private val getMyProfileUseCase: GetMyProfileUseCase + private val getMyProfileUseCase: GetMyProfileUseCase, + private val logoutUseCase: LogoutUseCase, + private val withdrawUseCase: WithdrawUseCase ) : BaseViewModel() { override val container: Container = container( @@ -72,10 +76,39 @@ class ProfileViewModel @Inject constructor( } } + private fun logout() { + safeLaunch { + logoutUseCase.invoke() + .onSuccess { + intent { + postSideEffect(ProfileSideEffect.NavigateToLogin) + } + } + .onFailure { /** no-op */ } + } + } + + private fun withdraw() { + safeLaunch { + intent { reduce { state.copy(isLoading = true) } } + + withdrawUseCase() + .onSuccess { + intent { + reduce { state.copy(isLoading = false, errorMessage = null) } + postSideEffect(ProfileSideEffect.NavigateToLogin) + } + }.onFailure { + intent { reduce { state.copy(isLoading = false, errorMessage = it.message) } } + } + } + } + override fun onAction(action: ProfileAction) { when (action) { + ProfileAction.LogoutConfirmed -> logout() + ProfileAction.WithdrawConfirmed -> withdraw() ProfileAction.EditProfileClicked -> intent { postSideEffect(ProfileSideEffect.NavigateToEditProfile) } - ProfileAction.SettingClicked -> intent { postSideEffect(ProfileSideEffect.NavigateToSetting) } ProfileAction.BackClicked -> intent { postSideEffect(ProfileSideEffect.NavigateToBack) } is ProfileAction.TicketClicked -> intent { postSideEffect(ProfileSideEffect.NavigateToDetail(action.id))} } @@ -96,15 +129,16 @@ data class ProfileData( ) sealed interface ProfileAction { - data object SettingClicked : ProfileAction data object EditProfileClicked : ProfileAction data object BackClicked : ProfileAction data class TicketClicked(val id: Long) : ProfileAction + data object LogoutConfirmed : ProfileAction + data object WithdrawConfirmed : ProfileAction } sealed interface ProfileSideEffect { - data object NavigateToSetting : ProfileSideEffect data object NavigateToEditProfile : ProfileSideEffect data object NavigateToBack : ProfileSideEffect data class NavigateToDetail(val id: Long) : ProfileSideEffect + data object NavigateToLogin : ProfileSideEffect } diff --git a/feature/profile/stability/profile-debug.stability b/feature/profile/stability/profile-debug.stability index 3ac14264..52332bc8 100644 --- a/feature/profile/stability/profile-debug.stability +++ b/feature/profile/stability/profile-debug.stability @@ -34,13 +34,12 @@ public fun com.idiotfrogs.profile.component.ProfileHeader(isChanged: kotlin.Bool - onSave: STABLE (function type) @Composable -public fun com.idiotfrogs.profile.component.ProfileHeader(modifier: androidx.compose.ui.Modifier, onBack: kotlin.Function0, onSetting: kotlin.Function0): kotlin.Unit +public fun com.idiotfrogs.profile.component.ProfileHeader(modifier: androidx.compose.ui.Modifier, onBack: kotlin.Function0): kotlin.Unit skippable: true restartable: true params: - modifier: STABLE (marked @Stable or @Immutable) - onBack: STABLE (function type) - - onSetting: STABLE (function type) @Composable public fun com.idiotfrogs.profile.component.ProfileTicketCard(imageUrl: kotlin.String, title: kotlin.String, date: kotlin.String, onClick: kotlin.Function0): kotlin.Unit @@ -82,3 +81,9 @@ public fun com.idiotfrogs.profile.profile.ProfileScreen(data: com.idiotfrogs.pro - data: STABLE (marked @Stable or @Immutable) - onAction: STABLE (function type) +@Composable +public fun com.idiotfrogs.profile.profile.rememberAppVersion(): kotlin.String + skippable: true + restartable: true + params: + diff --git a/feature/profile/stability/profile-release.stability b/feature/profile/stability/profile-release.stability index 3ac14264..52332bc8 100644 --- a/feature/profile/stability/profile-release.stability +++ b/feature/profile/stability/profile-release.stability @@ -34,13 +34,12 @@ public fun com.idiotfrogs.profile.component.ProfileHeader(isChanged: kotlin.Bool - onSave: STABLE (function type) @Composable -public fun com.idiotfrogs.profile.component.ProfileHeader(modifier: androidx.compose.ui.Modifier, onBack: kotlin.Function0, onSetting: kotlin.Function0): kotlin.Unit +public fun com.idiotfrogs.profile.component.ProfileHeader(modifier: androidx.compose.ui.Modifier, onBack: kotlin.Function0): kotlin.Unit skippable: true restartable: true params: - modifier: STABLE (marked @Stable or @Immutable) - onBack: STABLE (function type) - - onSetting: STABLE (function type) @Composable public fun com.idiotfrogs.profile.component.ProfileTicketCard(imageUrl: kotlin.String, title: kotlin.String, date: kotlin.String, onClick: kotlin.Function0): kotlin.Unit @@ -82,3 +81,9 @@ public fun com.idiotfrogs.profile.profile.ProfileScreen(data: com.idiotfrogs.pro - data: STABLE (marked @Stable or @Immutable) - onAction: STABLE (function type) +@Composable +public fun com.idiotfrogs.profile.profile.rememberAppVersion(): kotlin.String + skippable: true + restartable: true + params: + diff --git a/feature/setting/.gitignore b/feature/setting/.gitignore deleted file mode 100644 index 42afabfd..00000000 --- a/feature/setting/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/feature/setting/build.gradle.kts b/feature/setting/build.gradle.kts deleted file mode 100644 index a9ca2b44..00000000 --- a/feature/setting/build.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ -plugins { - id("convention.android.feature") -} - -android { - namespace = "com.idiotfrogs.setting" -} - -dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.test.ext) - androidTestImplementation(libs.androidx.test.espresso) -} \ No newline at end of file diff --git a/feature/setting/consumer-rules.pro b/feature/setting/consumer-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/feature/setting/proguard-rules.pro b/feature/setting/proguard-rules.pro deleted file mode 100644 index 481bb434..00000000 --- a/feature/setting/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# 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/setting/src/androidTest/java/com/idiotfrogs/setting/ExampleInstrumentedTest.kt b/feature/setting/src/androidTest/java/com/idiotfrogs/setting/ExampleInstrumentedTest.kt deleted file mode 100644 index 0f58e2fb..00000000 --- a/feature/setting/src/androidTest/java/com/idiotfrogs/setting/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.idiotfrogs.setting - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.idiotfrogs.setting.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/feature/setting/src/main/AndroidManifest.xml b/feature/setting/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e68..00000000 --- a/feature/setting/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/feature/setting/src/main/java/com/idiotfrogs/setting/SettingScreen.kt b/feature/setting/src/main/java/com/idiotfrogs/setting/SettingScreen.kt deleted file mode 100644 index b954e1a0..00000000 --- a/feature/setting/src/main/java/com/idiotfrogs/setting/SettingScreen.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.idiotfrogs.setting - -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.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding -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.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import com.idiotfrogs.designsystem.component.MSDialog -import com.idiotfrogs.designsystem.component.MSLoadingOverlay -import com.idiotfrogs.designsystem.component.MSText -import com.idiotfrogs.designsystem.component.MSTitleDialog -import com.idiotfrogs.designsystem.theme.MSTheme -import com.idiotfrogs.navigation.LocalComposeMSNavigator -import com.idiotfrogs.navigation.Routes -import com.idiotfrogs.setting.component.SettingHeader -import com.idiotfrogs.setting.component.SettingItem -import com.idiotfrogs.setting.component.SettingType -import org.orbitmvi.orbit.compose.collectAsState -import org.orbitmvi.orbit.compose.collectSideEffect - -@Composable -fun SettingRoute( - viewModel: SettingViewModel = hiltViewModel() -) { - val navigator = LocalComposeMSNavigator.current - val uiState by viewModel.collectAsState() - - viewModel.collectSideEffect { event -> - when (event) { - SettingSideEffect.NavigateToLogin -> { - navigator.clear() - navigator.navigate(Routes.Login) - } - SettingSideEffect.NavigateToBack -> navigator.popBackStack() - } - } - - Box(modifier = Modifier.fillMaxSize()) { - SettingScreen(onAction = viewModel::onAction) - - MSLoadingOverlay(visible = uiState.isLoading) - } -} - -@Composable -fun SettingScreen( - onAction: (SettingAction) -> Unit -) { - var showLogoutDialog by remember { mutableStateOf(false) } - var showWithdrawDialog by remember { mutableStateOf(false) } - - if (showLogoutDialog) { - MSTitleDialog( - title = "로그아웃", - confirmText = "로그아웃", - cancelText = "유지", - onConfirm = { - showLogoutDialog = false - onAction(SettingAction.LogoutConfirmed) - }, - onCancel = { showLogoutDialog = false }, - content = { - Spacer(modifier = Modifier.height(8.dp)) - MSText( - text = "메실에서 로그아웃 하시겠습니까?", - fontWeight = FontWeight.Normal, - fontSize = 16.dp, - color = MSTheme.color.greyG5 - ) - Spacer(modifier = Modifier.height(24.dp)) - } - ) - } - - if (showWithdrawDialog) { - MSTitleDialog( - title = "회원탈퇴", - confirmText = "탈퇴", - cancelText = "취소", - onConfirm = { - showWithdrawDialog = false - onAction(SettingAction.WithdrawConfirmed) - }, - onCancel = { showWithdrawDialog = false }, - content = { - Spacer(modifier = Modifier.height(8.dp)) - MSText( - text = "메실 회원을 탈퇴하시겠습니까?\n티켓에 저장된 내용은 삭제되지 않습니다.", - fontWeight = FontWeight.Normal, - fontSize = 16.dp, - color = MSTheme.color.greyG5 - ) - Spacer(modifier = Modifier.height(24.dp)) - } - ) - } - - Column( - modifier = Modifier - .fillMaxSize() - .background(MSTheme.color.white) - .systemBarsPadding(), - ) { - SettingHeader(onBack = { onAction(SettingAction.BackClicked) }) - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 20.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - SettingItem( - settingType = SettingType.Text(content = "v0.2"), - title = "앱 버전" - ) - SettingItem( - settingType = SettingType.Button, - title = "이용 약관", - onClick = { /** TODO: 약관 관련 정책 수립 필요 */ } - ) - SettingItem( - settingType = SettingType.Button, - title = "로그아웃", - onClick = { showLogoutDialog = true } - ) - SettingItem( - settingType = SettingType.Button, - title = "회원탈퇴", - titleColor = MSTheme.color.red, - onClick = { showWithdrawDialog = true } - ) - } - } -} - -@Preview -@Composable -private fun SettingScreenPreview() { - SettingScreen(onAction = {}) -} diff --git a/feature/setting/src/main/java/com/idiotfrogs/setting/SettingViewModel.kt b/feature/setting/src/main/java/com/idiotfrogs/setting/SettingViewModel.kt deleted file mode 100644 index 7e77e6a8..00000000 --- a/feature/setting/src/main/java/com/idiotfrogs/setting/SettingViewModel.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.idiotfrogs.setting - -import com.idiotfrogs.domain.usecase.auth.LogoutUseCase -import com.idiotfrogs.domain.usecase.user.WithdrawUseCase -import com.idiotfrogs.util.base.BaseUiState -import com.idiotfrogs.util.base.BaseViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import org.orbitmvi.orbit.Container -import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject - -@HiltViewModel -class SettingViewModel @Inject constructor( - private val logoutUseCase: LogoutUseCase, - private val withdrawUseCase: WithdrawUseCase -) : BaseViewModel() { - - override val container: Container = container(SettingUiState()) - - override fun onAction(action: SettingAction) { - when (action) { - SettingAction.LogoutConfirmed -> logout() - SettingAction.BackClicked -> intent { postSideEffect(SettingSideEffect.NavigateToBack) } - SettingAction.WithdrawConfirmed -> withdraw() - } - } - - private fun logout() { - safeLaunch { - logoutUseCase.invoke() - .onSuccess { - intent { - postSideEffect(SettingSideEffect.NavigateToLogin) - } - } - .onFailure { /** no-op */ } - } - } - - private fun withdraw() { - safeLaunch { - intent { reduce { state.copy(isLoading = true) } } - - withdrawUseCase() - .onSuccess { - intent { - reduce { state.copy(isLoading = false, errorMessage = null) } - postSideEffect(SettingSideEffect.NavigateToLogin) - } - }.onFailure { - intent { reduce { state.copy(isLoading = false, errorMessage = it.message) } } - } - } - } -} - -data class SettingUiState( - override val isLoading: Boolean = false, - override val errorMessage: String? = null, -) : BaseUiState - -sealed interface SettingAction { - data object LogoutConfirmed : SettingAction - data object BackClicked : SettingAction - data object WithdrawConfirmed : SettingAction -} - -sealed interface SettingSideEffect { - data object NavigateToLogin : SettingSideEffect - data object NavigateToBack : SettingSideEffect -} diff --git a/feature/setting/src/main/java/com/idiotfrogs/setting/component/SettingHeader.kt b/feature/setting/src/main/java/com/idiotfrogs/setting/component/SettingHeader.kt deleted file mode 100644 index 7252af64..00000000 --- a/feature/setting/src/main/java/com/idiotfrogs/setting/component/SettingHeader.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.idiotfrogs.setting.component - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -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.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.idiotfrogs.designsystem.component.MSText -import com.idiotfrogs.designsystem.theme.MSTheme -import com.idiotfrogs.designsystem.util.noRippleClickable -import com.idiotfrogs.resource.R - -@Composable -fun SettingHeader(onBack: () -> Unit) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(MSTheme.color.white) - .padding(horizontal = 20.dp, vertical = 16.dp) - ) { - Image( - modifier = Modifier - .size(24.dp) - .noRippleClickable(onClick = onBack), - painter = painterResource(R.drawable.ic_chevron_left), - contentDescription = "chevron_left", - ) - MSText( - modifier = Modifier.align(Alignment.Center), - text = "설정", - fontWeight = FontWeight.Bold, - fontSize = 20.dp, - color = MSTheme.color.greyG5 - ) - } -} - -@Preview -@Composable -private fun SettingHeaderPreview() { - SettingHeader(onBack = {}) -} \ No newline at end of file diff --git a/feature/setting/src/main/java/com/idiotfrogs/setting/component/SettingItem.kt b/feature/setting/src/main/java/com/idiotfrogs/setting/component/SettingItem.kt deleted file mode 100644 index 391f394c..00000000 --- a/feature/setting/src/main/java/com/idiotfrogs/setting/component/SettingItem.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.idiotfrogs.setting.component - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -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.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.idiotfrogs.designsystem.component.MSText -import com.idiotfrogs.designsystem.theme.MSTheme -import com.idiotfrogs.designsystem.util.noRippleClickable -import com.idiotfrogs.resource.R - -sealed interface SettingType { - data class Text(val content: String) : SettingType - data object Button : SettingType -} - -@Composable -fun SettingItem( - settingType: SettingType, - title: String, - titleColor: Color = MSTheme.color.greyG5, - onClick: (() -> Unit)? = null -) { - Row( - modifier = Modifier - .fillMaxWidth() - .background(MSTheme.color.white) - .then( - if (onClick != null) { - Modifier.noRippleClickable(onClick = onClick) - } else { - Modifier - } - ) - .padding(horizontal = 4.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - MSText( - text = title, - fontWeight = FontWeight.Medium, - fontSize = 16.dp, - color = titleColor - ) - when (settingType) { - is SettingType.Text -> { - MSText( - text = settingType.content, - fontWeight = FontWeight.Normal, - fontSize = 16.dp, - color = MSTheme.color.greyG4 - ) - } - SettingType.Button -> { - Image( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.ic_chevron_right), - contentDescription = "chevron_right" - ) - } - } - } -} - -@Preview -@Composable -fun SettingItemPreview() { - SettingItem( - settingType = SettingType.Button, - title = "로그아웃" - ) -} \ No newline at end of file diff --git a/feature/setting/src/test/java/com/idiotfrogs/setting/ExampleUnitTest.kt b/feature/setting/src/test/java/com/idiotfrogs/setting/ExampleUnitTest.kt deleted file mode 100644 index 1f2fc363..00000000 --- a/feature/setting/src/test/java/com/idiotfrogs/setting/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.idiotfrogs.setting - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/feature/setting/stability/setting-debug.stability b/feature/setting/stability/setting-debug.stability deleted file mode 100644 index 300406f1..00000000 --- a/feature/setting/stability/setting-debug.stability +++ /dev/null @@ -1,37 +0,0 @@ -// This file was automatically generated by Compose Stability Analyzer -// https://github.com/skydoves/compose-stability-analyzer -// -// Do not edit this file directly. To update it, run: -// ./gradlew :setting:stabilityDump - -@Composable -public fun com.idiotfrogs.setting.SettingRoute(viewModel: com.idiotfrogs.setting.SettingViewModel): kotlin.Unit - skippable: false - restartable: true - params: - - viewModel: UNSTABLE (has mutable properties or unstable members) - -@Composable -public fun com.idiotfrogs.setting.SettingScreen(onAction: kotlin.Function1): kotlin.Unit - skippable: true - restartable: true - params: - - onAction: STABLE (function type) - -@Composable -public fun com.idiotfrogs.setting.component.SettingHeader(onBack: kotlin.Function0): kotlin.Unit - skippable: true - restartable: true - params: - - onBack: STABLE (function type) - -@Composable -public fun com.idiotfrogs.setting.component.SettingItem(settingType: com.idiotfrogs.setting.component.SettingType, title: kotlin.String, titleColor: androidx.compose.ui.graphics.Color, onClick: kotlin.Function0?): kotlin.Unit - skippable: true - restartable: true - params: - - settingType: STABLE (class with no mutable properties) - - title: STABLE (String is immutable) - - titleColor: STABLE (marked @Stable or @Immutable) - - onClick: STABLE (function type) - diff --git a/feature/setting/stability/setting-release.stability b/feature/setting/stability/setting-release.stability deleted file mode 100644 index 300406f1..00000000 --- a/feature/setting/stability/setting-release.stability +++ /dev/null @@ -1,37 +0,0 @@ -// This file was automatically generated by Compose Stability Analyzer -// https://github.com/skydoves/compose-stability-analyzer -// -// Do not edit this file directly. To update it, run: -// ./gradlew :setting:stabilityDump - -@Composable -public fun com.idiotfrogs.setting.SettingRoute(viewModel: com.idiotfrogs.setting.SettingViewModel): kotlin.Unit - skippable: false - restartable: true - params: - - viewModel: UNSTABLE (has mutable properties or unstable members) - -@Composable -public fun com.idiotfrogs.setting.SettingScreen(onAction: kotlin.Function1): kotlin.Unit - skippable: true - restartable: true - params: - - onAction: STABLE (function type) - -@Composable -public fun com.idiotfrogs.setting.component.SettingHeader(onBack: kotlin.Function0): kotlin.Unit - skippable: true - restartable: true - params: - - onBack: STABLE (function type) - -@Composable -public fun com.idiotfrogs.setting.component.SettingItem(settingType: com.idiotfrogs.setting.component.SettingType, title: kotlin.String, titleColor: androidx.compose.ui.graphics.Color, onClick: kotlin.Function0?): kotlin.Unit - skippable: true - restartable: true - params: - - settingType: STABLE (class with no mutable properties) - - title: STABLE (String is immutable) - - titleColor: STABLE (marked @Stable or @Immutable) - - onClick: STABLE (function type) - diff --git a/settings.gradle.kts b/settings.gradle.kts index 364a9d72..0747df91 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -42,7 +42,6 @@ include(":feature:home") include(":feature:detail") include(":feature:profile") include(":feature:create") -include(":feature:setting") include(":feature:friend") include(":feature:splash") include(":feature:management")