diff --git a/app/build.gradle.kts b/app/build.gradle.kts index da20419f..887eda73 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 1 - versionName = "1.0" + versionName = "1.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/build.gradle.kts b/build.gradle.kts index ee715b43..90493c2b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,4 +8,5 @@ plugins { alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.hilt.android) apply false alias(libs.plugins.kotlin.ksp) apply false + alias(libs.plugins.kotlin.parcelize) apply false } \ No newline at end of file diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt index e99e9d20..7cd51036 100644 --- a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt +++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.goalpanzi.mission_mate.core.data.repository import com.goalpanzi.mission_mate.core.domain.repository.ProfileRepository import com.goalpanzi.mission_mate.core.network.service.ProfileService +import com.luckyoct.core.model.CharacterType import com.luckyoct.core.model.base.NetworkResult import com.luckyoct.core.model.request.SaveProfileRequest import javax.inject.Inject @@ -9,8 +10,8 @@ import javax.inject.Inject class ProfileRepositoryImpl @Inject constructor( private val profileService: ProfileService ): ProfileRepository { - override suspend fun saveProfile(nickname: String, index: Int): NetworkResult = handleResult { - val request = SaveProfileRequest.createRequest(nickname, index) + override suspend fun saveProfile(nickname: String, type: CharacterType): NetworkResult = handleResult { + val request = SaveProfileRequest.createRequest(nickname, type) profileService.saveProfile(request) } } \ No newline at end of file diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index 497a8b02..85ef0949 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -45,4 +45,6 @@ dependencies { ksp(libs.hilt.compiler) implementation(libs.hilt.android) + + implementation(project(":core:model")) } \ No newline at end of file diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSource.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSource.kt index e04d5ce1..e18f3cce 100644 --- a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSource.kt +++ b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSource.kt @@ -1,7 +1,10 @@ package com.goalpanzi.mission_mate.core.datastore.datasource +import com.luckyoct.core.model.UserProfile import kotlinx.coroutines.flow.Flow interface DefaultDataSource { fun clearUserData() : Flow + fun setUserProfile(data: UserProfile) : Flow + fun getUserProfile() : Flow } \ No newline at end of file diff --git a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSourceImpl.kt b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSourceImpl.kt index 34e88b0c..fe8ce237 100644 --- a/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSourceImpl.kt +++ b/core/datastore/src/main/java/com/goalpanzi/mission_mate/core/datastore/datasource/DefaultDataSourceImpl.kt @@ -3,17 +3,45 @@ package com.goalpanzi.mission_mate.core.datastore.datasource import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.luckyoct.core.model.CharacterType +import com.luckyoct.core.model.UserProfile import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map import javax.inject.Inject class DefaultDataSourceImpl @Inject constructor( private val dataStore: DataStore ) : DefaultDataSource { + + object PreferencesKey { + val USER_NICKNAME = stringPreferencesKey("USER_NICKNAME") + val USER_CHARACTER = stringPreferencesKey("USER_CHARACTER") + } + override fun clearUserData(): Flow = flow { dataStore.edit { preferences -> preferences.clear() } emit(Unit) } + + override fun setUserProfile(data: UserProfile): Flow = flow { + dataStore.edit { preferences -> + preferences[PreferencesKey.USER_NICKNAME] = data.nickname + preferences[PreferencesKey.USER_CHARACTER] = data.characterType.name.uppercase() + } + emit(Unit) + } + + override fun getUserProfile(): Flow = dataStore.data.map { preferences -> + val nickname = preferences[PreferencesKey.USER_NICKNAME] + val character = preferences[PreferencesKey.USER_CHARACTER] + if (nickname != null && character != null) { + UserProfile(nickname, CharacterType.valueOf(character)) + } else { + null + } + } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/AuthRepository.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/AuthRepository.kt index a930d4c9..331a4210 100644 --- a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/AuthRepository.kt +++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/AuthRepository.kt @@ -1,7 +1,7 @@ package com.goalpanzi.mission_mate.core.domain.repository import com.goalpanzi.mission_mate.core.network.ResultHandler -import com.luckyoct.core.model.GoogleLogin +import com.luckyoct.core.model.response.GoogleLogin import com.luckyoct.core.model.base.NetworkResult interface AuthRepository : ResultHandler { diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt index 55400ac6..174add5e 100644 --- a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt +++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt @@ -1,8 +1,9 @@ package com.goalpanzi.mission_mate.core.domain.repository import com.goalpanzi.mission_mate.core.network.ResultHandler +import com.luckyoct.core.model.CharacterType import com.luckyoct.core.model.base.NetworkResult interface ProfileRepository: ResultHandler { - suspend fun saveProfile(nickname: String, index: Int): NetworkResult + suspend fun saveProfile(nickname: String, type: CharacterType): NetworkResult } \ No newline at end of file diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LoginUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LoginUseCase.kt index 244bbf71..c1e39216 100644 --- a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LoginUseCase.kt +++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/LoginUseCase.kt @@ -1,15 +1,20 @@ package com.goalpanzi.mission_mate.core.domain.usecase import com.goalpanzi.mission_mate.core.datastore.datasource.AuthDataSource +import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource import com.goalpanzi.mission_mate.core.domain.repository.AuthRepository -import com.luckyoct.core.model.GoogleLogin +import com.luckyoct.core.model.UserProfile import com.luckyoct.core.model.base.NetworkResult +import com.luckyoct.core.model.response.GoogleLogin +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import javax.inject.Inject class LoginUseCase @Inject constructor( private val authRepository: AuthRepository, - private val authDataSource: AuthDataSource + private val authDataSource: AuthDataSource, + private val defaultDataSource: DefaultDataSource ) { suspend fun requestGoogleLogin(email: String): GoogleLogin? { return when (val response = authRepository.requestGoogleLogin(email)) { @@ -17,10 +22,25 @@ class LoginUseCase @Inject constructor( response.data.also { authDataSource.setAccessToken(it.accessToken).first() authDataSource.setRefreshToken(it.refreshToken).first() + (it.nickname to it.characterType).let { (nickname, character) -> + if (nickname != null && character != null) { + defaultDataSource.setUserProfile( + UserProfile(nickname, character) + ).first() + } + } } } + is NetworkResult.Error, is NetworkResult.Exception -> null } } + fun isNewUser(): Boolean = runBlocking(Dispatchers.IO) { + authDataSource.getAccessToken().first() == null + } + + fun getCachedUserData(): UserProfile? = runBlocking(Dispatchers.IO) { + defaultDataSource.getUserProfile().first() + } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt index 89c23033..1e5791f2 100644 --- a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt +++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt @@ -1,10 +1,21 @@ package com.goalpanzi.mission_mate.core.domain.usecase +import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource import com.goalpanzi.mission_mate.core.domain.repository.ProfileRepository +import com.luckyoct.core.model.CharacterType +import com.luckyoct.core.model.UserProfile +import com.luckyoct.core.model.base.NetworkResult +import kotlinx.coroutines.flow.first import javax.inject.Inject class ProfileUseCase @Inject constructor( - private val profileRepository: ProfileRepository + private val profileRepository: ProfileRepository, + private val defaultDataSource: DefaultDataSource ) { - suspend fun saveProfile(nickname: String, index: Int) = profileRepository.saveProfile(nickname, index) + suspend fun saveProfile(nickname: String, type: CharacterType) = profileRepository.saveProfile(nickname, type).also { + if (it is NetworkResult.Success) { + defaultDataSource.setUserProfile(UserProfile(nickname, type)).first() + } + } + suspend fun getProfile(): UserProfile? = defaultDataSource.getUserProfile().first() } \ No newline at end of file diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index f8036ed9..b39d9ec7 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.kotlin.plugin.serialization) + alias(libs.plugins.kotlin.parcelize) } android { diff --git a/core/model/src/main/java/com/luckyoct/core/model/CharacterType.kt b/core/model/src/main/java/com/luckyoct/core/model/CharacterType.kt new file mode 100644 index 00000000..4dfbbb88 --- /dev/null +++ b/core/model/src/main/java/com/luckyoct/core/model/CharacterType.kt @@ -0,0 +1,24 @@ +package com.luckyoct.core.model + +import kotlinx.serialization.SerialName + +enum class CharacterType { + + @SerialName("RABBIT") + RABBIT, + + @SerialName("CAT") + CAT, + + @SerialName("DOG") + DOG, + + @SerialName("PANDA") + PANDA, + + @SerialName("BEAR") + BEAR, + + @SerialName("BIRD") + BIRD +} \ No newline at end of file diff --git a/core/model/src/main/java/com/luckyoct/core/model/UserProfile.kt b/core/model/src/main/java/com/luckyoct/core/model/UserProfile.kt new file mode 100644 index 00000000..d9a98f03 --- /dev/null +++ b/core/model/src/main/java/com/luckyoct/core/model/UserProfile.kt @@ -0,0 +1,6 @@ +package com.luckyoct.core.model + +data class UserProfile( + val nickname: String, + val characterType: CharacterType +) \ No newline at end of file diff --git a/core/model/src/main/java/com/luckyoct/core/model/request/SaveProfileRequest.kt b/core/model/src/main/java/com/luckyoct/core/model/request/SaveProfileRequest.kt index 5b961ede..f4c69505 100644 --- a/core/model/src/main/java/com/luckyoct/core/model/request/SaveProfileRequest.kt +++ b/core/model/src/main/java/com/luckyoct/core/model/request/SaveProfileRequest.kt @@ -1,20 +1,17 @@ package com.luckyoct.core.model.request +import com.luckyoct.core.model.CharacterType import kotlinx.serialization.Serializable -enum class CharacterType { - RABBIT, CAT, DOG, PANDA, BEAR, BIRD -} - @Serializable data class SaveProfileRequest( val nickname: String, val characterType: String, ) { companion object { - fun createRequest(nickname: String, index: Int) = SaveProfileRequest( + fun createRequest(nickname: String, type: CharacterType) = SaveProfileRequest( nickname = nickname, - characterType = CharacterType.entries[index].name.uppercase() + characterType = type.name.uppercase() ) } } diff --git a/core/model/src/main/java/com/luckyoct/core/model/GoogleLogin.kt b/core/model/src/main/java/com/luckyoct/core/model/response/GoogleLogin.kt similarity index 53% rename from core/model/src/main/java/com/luckyoct/core/model/GoogleLogin.kt rename to core/model/src/main/java/com/luckyoct/core/model/response/GoogleLogin.kt index 6c31421d..a84d6628 100644 --- a/core/model/src/main/java/com/luckyoct/core/model/GoogleLogin.kt +++ b/core/model/src/main/java/com/luckyoct/core/model/response/GoogleLogin.kt @@ -1,10 +1,13 @@ -package com.luckyoct.core.model +package com.luckyoct.core.model.response +import com.luckyoct.core.model.CharacterType import kotlinx.serialization.Serializable @Serializable data class GoogleLogin( val accessToken: String, val refreshToken: String, + val nickname: String?, + val characterType: CharacterType?, val isProfileSet: Boolean ) diff --git a/core/model/src/main/java/com/luckyoct/core/model/response/MissionBoardMembersResponse.kt b/core/model/src/main/java/com/luckyoct/core/model/response/MissionBoardMembersResponse.kt index 38c4ac36..4d7a2366 100644 --- a/core/model/src/main/java/com/luckyoct/core/model/response/MissionBoardMembersResponse.kt +++ b/core/model/src/main/java/com/luckyoct/core/model/response/MissionBoardMembersResponse.kt @@ -1,6 +1,6 @@ package com.luckyoct.core.model.response -import com.luckyoct.core.model.request.CharacterType +import com.luckyoct.core.model.CharacterType import kotlinx.serialization.Serializable @Serializable diff --git a/core/model/src/main/java/com/luckyoct/core/model/response/MissionVerificationResponse.kt b/core/model/src/main/java/com/luckyoct/core/model/response/MissionVerificationResponse.kt index faf150ae..b1f849c3 100644 --- a/core/model/src/main/java/com/luckyoct/core/model/response/MissionVerificationResponse.kt +++ b/core/model/src/main/java/com/luckyoct/core/model/response/MissionVerificationResponse.kt @@ -1,6 +1,6 @@ package com.luckyoct.core.model.response -import com.luckyoct.core.model.request.CharacterType +import com.luckyoct.core.model.CharacterType import kotlinx.serialization.Serializable @Serializable diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/NetworkModule.kt index de264cf2..63a96c09 100644 --- a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/di/NetworkModule.kt @@ -7,18 +7,13 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.serialization.json.Json -import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Converter import retrofit2.Retrofit import retrofit2.converter.kotlinx.serialization.asConverterFactory -import java.security.KeyStore import javax.inject.Singleton -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509TrustManager @Module @InstallIn(SingletonComponent::class) @@ -36,16 +31,9 @@ internal object NetworkModule { httpLoggingInterceptor: HttpLoggingInterceptor, tokenInterceptor: TokenInterceptor ): OkHttpClient { - // TLS 대응 - val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - trustManagerFactory.init(null as KeyStore?) - val trustManager = trustManagerFactory.trustManagers[0] as X509TrustManager - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, arrayOf(trustManager), java.security.SecureRandom()) return OkHttpClient.Builder() .addInterceptor(tokenInterceptor) - .sslSocketFactory(sslContext.socketFactory, trustManager) .addInterceptor(httpLoggingInterceptor) .build() } diff --git a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/LoginService.kt b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/LoginService.kt index 81653286..9f9653a5 100644 --- a/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/LoginService.kt +++ b/core/network/src/main/java/com/goalpanzi/mission_mate/core/network/service/LoginService.kt @@ -1,6 +1,6 @@ package com.goalpanzi.mission_mate.core.network.service -import com.luckyoct.core.model.GoogleLogin +import com.luckyoct.core.model.response.GoogleLogin import com.luckyoct.core.model.request.GoogleLoginRequest import retrofit2.Response import retrofit2.http.Body diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt index 2eb52c6f..0b159155 100644 --- a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt +++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt @@ -1,7 +1,7 @@ package com.goalpanzi.mission_mate.feature.board.model import androidx.annotation.DrawableRes -import com.luckyoct.core.model.request.CharacterType +import com.luckyoct.core.model.CharacterType enum class Character( @DrawableRes val imageId: Int, diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index ef44c187..380bf2e6 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { implementation(project(":core:designsystem")) implementation(project(":core:navigation")) implementation(project(":core:domain")) + implementation(project(":core:model")) implementation(project(":feature:login")) implementation(project(":feature:onboarding")) implementation(project(":feature:profile")) diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt index 4740d8ec..fdc79fda 100644 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainActivity.kt @@ -6,27 +6,42 @@ import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels import com.goalpanzi.mission_mate.core.designsystem.theme.MissionmateTheme +import com.goalpanzi.mission_mate.core.domain.usecase.LoginUseCase import com.goalpanzi.mission_mate.core.main.component.MainNavigator import com.goalpanzi.mission_mate.core.main.component.rememberMainNavigator +import com.goalpanzi.mission_mate.core.navigation.RouteModel import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { - private val viewModel: MainViewModel by viewModels() + @Inject + lateinit var loginUseCase: LoginUseCase override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge( statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT) ) + val isNewUser = loginUseCase.isNewUser() + val user = loginUseCase.getCachedUserData() + setContent { val navigator: MainNavigator = rememberMainNavigator() MissionmateTheme { MainScreen( navigator = navigator, + startDestination = if (isNewUser) { + RouteModel.Login + } else { + if (user == null) { + RouteModel.Profile.Create + } else { + RouteModel.Onboarding + } + } ) } } diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainScreen.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainScreen.kt index 3a5366ab..6eb85be5 100644 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainScreen.kt +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainScreen.kt @@ -6,19 +6,23 @@ import androidx.compose.ui.Modifier import com.goalpanzi.mission_mate.core.main.component.MainNavHost import com.goalpanzi.mission_mate.core.main.component.MainNavigator import com.goalpanzi.mission_mate.core.main.component.rememberMainNavigator +import com.goalpanzi.mission_mate.core.navigation.RouteModel @Composable internal fun MainScreen( navigator: MainNavigator = rememberMainNavigator(), + startDestination: RouteModel ) { MainScreenContent( - navigator = navigator + navigator = navigator, + startDestination = startDestination ) } @Composable private fun MainScreenContent( navigator: MainNavigator, + startDestination: RouteModel, modifier: Modifier = Modifier ) { Scaffold( @@ -26,6 +30,7 @@ private fun MainScreenContent( content = { padding -> MainNavHost( navigator = navigator, + startDestination = startDestination, padding = padding ) } diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainViewModel.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainViewModel.kt deleted file mode 100644 index dd508655..00000000 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/MainViewModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.goalpanzi.mission_mate.core.main - -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class MainViewModel @Inject constructor( - -): ViewModel() { - - // TODO : flow to get isAuthorized - val isAuthorized = false -} \ No newline at end of file diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt index 57f2f509..9f22a99f 100644 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt @@ -8,7 +8,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost -import androidx.navigation.navOptions import com.goalpanzi.mission_mate.core.navigation.RouteModel import com.goalpanzi.mission_mate.feature.board.boardNavGraph import com.goalpanzi.mission_mate.feature.login.loginNavGraph @@ -26,6 +25,7 @@ import com.luckyoct.feature.setting.navigation.settingNavGraph internal fun MainNavHost( modifier: Modifier = Modifier, navigator: MainNavigator, + startDestination: RouteModel, padding: PaddingValues ) { Box( @@ -35,7 +35,7 @@ internal fun MainNavHost( ) { NavHost( navController = navigator.navController, - startDestination = navigator.startDestination + startDestination = startDestination ) { loginNavGraph( onLoginSuccess = { if (it) navigator.navigationToOnboarding() else navigator.navigateToProfileCreate() } @@ -76,7 +76,6 @@ internal fun MainNavHost( settingNavGraph( onBackClick = { navigator.popBackStack() }, onClickProfileSetting = { navigator.navigateToProfileSetting() }, - onClickInquiry = { navigator.navigationToInquiry() }, onClickServicePolicy = { navigator.navigationToServicePolicy() }, onClickPrivacyPolicy = { navigator.navigationToPrivacyPolicy() }, onClickLogout = { navigator.navigateToLogin() } diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt index 416089f9..4900da02 100644 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt @@ -62,10 +62,6 @@ class MainNavigator( navController.navigateToSetting() } - fun navigationToInquiry() { - navController.navigateToInquiry() - } - fun navigationToServicePolicy() { navController.navigateToServicePolicy() } diff --git a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileNavigation.kt b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileNavigation.kt index 561c1ed3..0741d032 100644 --- a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileNavigation.kt +++ b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileNavigation.kt @@ -24,14 +24,14 @@ fun NavGraphBuilder.profileNavGraph( composable { ProfileRoute( profileSettingType = ProfileSettingType.CREATE, - onSaveSuccess = { onSaveSuccess() } + onSaveSuccess = onSaveSuccess ) } composable { ProfileRoute( profileSettingType = ProfileSettingType.SETTING, - onSaveSuccess = { onSaveSuccess() }, - onBackClick = { onBackClick() } + onSaveSuccess = onBackClick, + onBackClick = onBackClick ) } } \ No newline at end of file diff --git a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileScreen.kt b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileScreen.kt index 3b33862e..2fb3c419 100644 --- a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileScreen.kt +++ b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileScreen.kt @@ -9,21 +9,24 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,6 +37,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.paint +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -42,17 +47,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextFieldGroup import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9 -import com.goalpanzi.mission_mate.core.designsystem.theme.ColorPink_FFFFE4E4 import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType +import com.luckyoct.core.model.CharacterType import com.luckyoct.feature.profile.model.CharacterListItem -import com.luckyoct.feature.profile.model.ProfileEvent +import com.luckyoct.feature.profile.model.ProfileUiState import dagger.hilt.android.EntryPointAccessors import kotlinx.coroutines.flow.collectLatest @@ -74,46 +80,51 @@ fun ProfileRoute( onBackClick: (() -> Unit)? = null ) { val viewModel = profileViewModel(profileSettingType = profileSettingType) - val characters = viewModel.characters.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val isInvalidNickname by viewModel.isInvalidNickname.collectAsStateWithLifecycle() LaunchedEffect(true) { - viewModel.event.collectLatest { - when (it) { - ProfileEvent.Success -> onSaveSuccess() - else -> return@collectLatest - } + viewModel.isSaveSuccess.collectLatest { + if (it) onSaveSuccess() } } - ProfileScreen( - profileSettingType = profileSettingType, - characters = characters.value, - onclickCharacter = { viewModel.selectCharacter(it) }, - onClickSave = { viewModel.saveProfile(it) }, - onBackClick = { onBackClick?.invoke() } - ) + Box( + modifier = Modifier + .fillMaxSize() + .background(color = ColorWhite_FFFFFFFF) + .systemBarsPadding() + .navigationBarsPadding() + .imePadding() + ) { + ProfileContent( + modifier = modifier, + uiState = uiState, + profileSettingType = profileSettingType, + onClickCharacter = { viewModel.selectCharacter(it) }, + onClickSave = { viewModel.saveProfile(it) }, + onBackClick = onBackClick, + isInvalidNickname = isInvalidNickname, + resetNicknameErrorState = { viewModel.resetNicknameErrorState() } + ) + } } @Composable -fun ProfileScreen( +fun ProfileContent( modifier: Modifier = Modifier, + uiState: ProfileUiState, profileSettingType: ProfileSettingType, - characters: List, - onclickCharacter: (CharacterListItem) -> Unit, - onClickSave: (String) -> Unit, - onBackClick: (() -> Unit)? = null + onClickCharacter: (CharacterListItem) -> Unit = {}, + onClickSave: (String) -> Unit = {}, + onBackClick: (() -> Unit)? = null, + isInvalidNickname: Boolean, + resetNicknameErrorState: () -> Unit ) { - - var nicknameInput by remember { mutableStateOf("") } - val scrollState = rememberScrollState() - val regex = Regex("^[가-힣ㅏ-ㅣㄱ-ㅎa-zA-Z0-9]{1,6}$") - Column( modifier = modifier .fillMaxSize() .background(color = ColorWhite_FFFFFFFF) - .imePadding() - .statusBarsPadding() ) { if (profileSettingType == ProfileSettingType.SETTING) { MissionMateTopAppBar( @@ -122,74 +133,129 @@ fun ProfileScreen( onNavigationClick = { onBackClick?.invoke() } ) } - Column( - modifier = modifier - .padding(bottom = 18.dp) - .fillMaxWidth() - .weight(1f) - .verticalScroll(scrollState) - ) { - Text( - text = stringResource(id = when (profileSettingType) { - ProfileSettingType.CREATE -> R.string.profile_create - ProfileSettingType.SETTING -> R.string.profile_setting_title - }), - modifier = modifier - .align(Alignment.CenterHorizontally) - .padding(top = 48.dp), - style = MissionMateTypography.heading_sm_bold, - color = ColorGray1_FF404249 - ) + when (uiState) { + ProfileUiState.Loading -> { + ProfileLoading() + } - characters.find { it.isSelected }?.let { - Box( - modifier = modifier - .padding(top = 32.dp) - .size(220.dp) - .background(color = it.backgroundColor, shape = CircleShape) - .align(Alignment.CenterHorizontally) - ) { - CharacterLargeImage(imageResId = it.imageResId) - } + is ProfileUiState.Success -> { + ProfileScreen( + modifier = modifier, + profileSettingType = profileSettingType, + initialNickname = uiState.nickname, + characters = uiState.characterList, + onClickCharacter = onClickCharacter, + onClickSave = onClickSave, + isInvalidNickname = isInvalidNickname, + resetNicknameErrorState = resetNicknameErrorState + ) } - CharacterRow( - characters = characters, - onClick = onclickCharacter - ) + } + } +} - MissionMateTextFieldGroup( +@Composable +fun ColumnScope.ProfileScreen( + modifier: Modifier = Modifier, + initialNickname: String, + profileSettingType: ProfileSettingType, + characters: List, + onClickCharacter: (CharacterListItem) -> Unit, + onClickSave: (String) -> Unit, + isInvalidNickname: Boolean, + resetNicknameErrorState: () -> Unit = {} +) { + var nicknameInput by remember { mutableStateOf(initialNickname) } + val scrollState = rememberScrollState() + val regex = Regex("^[가-힣ㅏ-ㅣㄱ-ㅎa-zA-Z0-9]{1,6}$") + + Column( + modifier = modifier + .padding(bottom = 18.dp) + .weight(1f) + .fillMaxWidth() + .verticalScroll(scrollState) + .imePadding() + ) { + Text( + text = stringResource( + id = when (profileSettingType) { + ProfileSettingType.CREATE -> R.string.profile_create + ProfileSettingType.SETTING -> R.string.profile_setting_title + } + ), + modifier = modifier + .align(Alignment.CenterHorizontally) + .padding(top = 48.dp), + style = MissionMateTypography.heading_sm_bold, + color = ColorGray1_FF404249 + ) + + characters.find { it.isSelected }?.let { + Box( modifier = modifier - .padding(top = 38.dp, start = 24.dp, end = 24.dp) - .fillMaxWidth() - .wrapContentHeight(), - text = nicknameInput, - onValueChange = { if (regex.matches(it) || it.isEmpty()) nicknameInput = it }, - hintId = R.string.nickname_hint, - guidanceId = R.string.nickname_input_guide - ) + .padding(top = 32.dp) + .size(220.dp) + .align(Alignment.CenterHorizontally) + ) { + CharacterLargeImage( + imageResId = it.imageResId, + backgroundResId = it.backgroundResId + ) + } } + CharacterRow( + characters = characters, + onClick = onClickCharacter + ) - MissionMateTextButton( + MissionMateTextFieldGroup( modifier = modifier - .padding(bottom = 36.dp, start = 24.dp, end = 24.dp) - .fillMaxWidth(), - textId = R.string.save, - onClick = { onClickSave(nicknameInput) } + .padding(top = 38.dp, start = 24.dp, end = 24.dp) + .fillMaxWidth() + .wrapContentHeight(), + text = nicknameInput, + onValueChange = { + if (regex.matches(it) || it.isEmpty()) { + nicknameInput = it + } + resetNicknameErrorState() + }, + hintId = R.string.nickname_hint, + guidanceId = if (isInvalidNickname) R.string.err_duplicated_nickname else R.string.nickname_input_guide, + isError = isInvalidNickname ) } + + MissionMateTextButton( + modifier = modifier + .padding(bottom = 36.dp, start = 24.dp, end = 24.dp) + .fillMaxWidth(), + textId = R.string.save, + buttonType = if (nicknameInput.trim().isEmpty()) { + MissionMateButtonType.DISABLED + } else { + MissionMateButtonType.ACTIVE + }, + onClick = { onClickSave(nicknameInput) } + ) } @Composable fun CharacterLargeImage( modifier: Modifier = Modifier, @DrawableRes imageResId: Int, + @DrawableRes backgroundResId: Int, ) { Image( - modifier = modifier - .padding(10.dp) - .fillMaxSize(), painter = painterResource(id = imageResId), - contentDescription = "" + contentDescription = null, + modifier = modifier + .fillMaxSize() + .paint( + painter = painterResource(backgroundResId), + contentScale = ContentScale.FillWidth, + ) ) } @@ -199,11 +265,20 @@ fun CharacterRow( characters: List, onClick: (CharacterListItem) -> Unit ) { + val scrollState = rememberLazyListState() + + LaunchedEffect(key1 = characters) { + characters.indexOfFirst { it.isSelected }.takeIf { it > 0 }?.let { + scrollState.animateScrollToItem(it - 1) + } + } + LazyRow( modifier = modifier .padding(top = 18.dp), horizontalArrangement = Arrangement.spacedBy(space = 8.dp), - contentPadding = PaddingValues(horizontal = 24.dp) + contentPadding = PaddingValues(horizontal = 24.dp), + state = scrollState ) { items(items = characters, key = { it.imageResId }) { CharacterElement( @@ -248,39 +323,52 @@ fun CharacterElement( } } +@Composable +private fun ProfileLoading() { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } +} + @Preview @Composable -fun ProfileScreenPreview() { +fun ColumnScope.ProfileScreenPreview() { ProfileScreen( profileSettingType = ProfileSettingType.CREATE, + initialNickname = "", characters = listOf( CharacterListItem( + type = CharacterType.RABBIT, imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_rabbit_selected, nameResId = R.string.rabbit_name, isSelected = true, - backgroundColor = ColorPink_FFFFE4E4 + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_rabbit ), CharacterListItem( + type = CharacterType.CAT, imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected, nameResId = R.string.cat_name, isSelected = false, - backgroundColor = ColorPink_FFFFE4E4 + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_cat ), CharacterListItem( + type = CharacterType.DOG, imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_dog_selected, nameResId = R.string.dog_name, isSelected = false, - backgroundColor = ColorPink_FFFFE4E4 + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_dog ), CharacterListItem( + type = CharacterType.PANDA, imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_panda_selected, nameResId = R.string.panda_name, isSelected = false, - backgroundColor = ColorPink_FFFFE4E4 + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_panda ), ), - onclickCharacter = {}, - onClickSave = {} + onClickCharacter = {}, + onClickSave = {}, + isInvalidNickname = false ) } @@ -289,10 +377,11 @@ fun ProfileScreenPreview() { fun CharacterElementPreview() { CharacterElement( character = CharacterListItem( + type = CharacterType.CAT, imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected, nameResId = R.string.cat_name, isSelected = false, - backgroundColor = ColorPink_FFFFE4E4 + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_cat ) ) } \ No newline at end of file diff --git a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileViewModel.kt b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileViewModel.kt index 41ed8210..5298d4c1 100644 --- a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileViewModel.kt +++ b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileViewModel.kt @@ -1,5 +1,6 @@ package com.luckyoct.feature.profile +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -10,9 +11,11 @@ import com.goalpanzi.mission_mate.core.designsystem.theme.ColorLightGreen_FFC2E7 import com.goalpanzi.mission_mate.core.designsystem.theme.ColorLightYellow_FFFFE59A import com.goalpanzi.mission_mate.core.designsystem.theme.ColorPink_FFFFE4E4 import com.goalpanzi.mission_mate.core.domain.usecase.ProfileUseCase +import com.luckyoct.core.model.CharacterType +import com.luckyoct.core.model.UserProfile import com.luckyoct.core.model.base.NetworkResult import com.luckyoct.feature.profile.model.CharacterListItem -import com.luckyoct.feature.profile.model.ProfileEvent +import com.luckyoct.feature.profile.model.ProfileUiState import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -25,7 +28,7 @@ import kotlinx.coroutines.launch class ProfileViewModel @AssistedInject constructor( @Assisted private val profileSettingType: ProfileSettingType, private val profileUseCase: ProfileUseCase -): ViewModel() { +) : ViewModel() { @AssistedFactory interface Factory { @@ -44,77 +47,78 @@ class ProfileViewModel @AssistedInject constructor( } } - private val defaultImageIds = listOf( - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_rabbit_selected, - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected, - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_dog_selected, - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_panda_selected, - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bear_selected, - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bird_selected - ) - private val defaultNameIds = listOf( - R.string.rabbit_name, - R.string.cat_name, - R.string.dog_name, - R.string.panda_name, - R.string.bear_name, - R.string.bird_name - ) - private val defaultColors = listOf( - ColorPink_FFFFE4E4, - ColorBlue_FFBFD7FF, - ColorLightYellow_FFFFE59A, - ColorLightGreen_FFC2E792, - ColorLightBrown_FFF7D8B3, - ColorLightBlue_FFBCE7FF - ) + private val defaultCharacters = CharacterListItem.createDefaultList() - private val _event = MutableSharedFlow() - val event = _event.asSharedFlow() + private val _uiState = MutableStateFlow(ProfileUiState.Loading) + val uiState = _uiState.asStateFlow() - private val _characters = MutableStateFlow( - when (profileSettingType) { - ProfileSettingType.CREATE -> { - defaultImageIds.indices.map { - CharacterListItem( - imageResId = defaultImageIds[it], - nameResId = defaultNameIds[it], - isSelected = it == 0, - backgroundColor = defaultColors[it] + private val _isInvalidNickname = MutableStateFlow(false) + val isInvalidNickname = _isInvalidNickname.asStateFlow() + + private val _isSaveSuccess = MutableSharedFlow() + val isSaveSuccess = _isSaveSuccess.asSharedFlow() + + init { + viewModelScope.launch { + when (profileSettingType) { + ProfileSettingType.CREATE -> { + _uiState.value = ProfileUiState.Success( + nickname = "", + characterList = defaultCharacters.toMutableList().apply { + set(0, get(0).copy(isSelected = true)) + } ) } - } - // TODO : set my character selected - ProfileSettingType.SETTING -> { - defaultImageIds.indices.map { - CharacterListItem( - imageResId = defaultImageIds[it], - nameResId = defaultNameIds[it], - isSelected = false, - backgroundColor = defaultColors[it] + + ProfileSettingType.SETTING -> { + val userProfile = profileUseCase.getProfile() + ?: UserProfile( + nickname = "", + characterType = CharacterType.RABBIT + ) // TODO : API + _uiState.value = ProfileUiState.Success( + nickname = userProfile.nickname, + characterList = defaultCharacters.toMutableList().apply { + val index = indexOfFirst { it.type == userProfile.characterType } + set(index, get(index).copy(isSelected = true)) + } ) } } } - ) - val characters = _characters.asStateFlow() + } fun selectCharacter(character: CharacterListItem) { - _characters.value = _characters.value.map { - it.copy(isSelected = it == character) - } + val state = uiState.value as? ProfileUiState.Success ?: return + _uiState.value = state.copy( + characterList = state.characterList.map { + it.copy(isSelected = it == character) + } + ) + } + + fun resetNicknameErrorState() { + _isInvalidNickname.value = false } fun saveProfile(nickname: String) { if (nickname.isEmpty()) return - viewModelScope.launch { - val response = profileUseCase.saveProfile(nickname, characters.value.indexOfFirst { it.isSelected }) - when (response) { + val selectedItem = (uiState.value as? ProfileUiState.Success)?.characterList?.find { + it.isSelected + } ?: return@launch + + when(val response = profileUseCase.saveProfile(nickname, selectedItem.type)) { is NetworkResult.Success -> { - _event.emit(ProfileEvent.Success) + profileUseCase.saveProfile(nickname, selectedItem.type) + _isSaveSuccess.emit(true) + } + is NetworkResult.Exception -> {} + is NetworkResult.Error -> { + if (response.code == 409) { + _isInvalidNickname.emit(true) + } } - else -> return@launch } } } diff --git a/feature/profile/src/main/java/com/luckyoct/feature/profile/model/CharacterListItem.kt b/feature/profile/src/main/java/com/luckyoct/feature/profile/model/CharacterListItem.kt index 824e9390..6b0cf1b9 100644 --- a/feature/profile/src/main/java/com/luckyoct/feature/profile/model/CharacterListItem.kt +++ b/feature/profile/src/main/java/com/luckyoct/feature/profile/model/CharacterListItem.kt @@ -2,11 +2,54 @@ package com.luckyoct.feature.profile.model import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.compose.ui.graphics.Color +import com.luckyoct.core.model.CharacterType +import com.luckyoct.feature.profile.R data class CharacterListItem( + val type: CharacterType, @DrawableRes val imageResId: Int, @StringRes val nameResId: Int, - val isSelected: Boolean, - val backgroundColor: Color -) + val isSelected: Boolean = false, + @DrawableRes val backgroundResId: Int +) { + companion object { + fun createDefaultList() = listOf( + CharacterListItem( + type = CharacterType.RABBIT, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_rabbit_selected, + nameResId = R.string.rabbit_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_rabbit + ), + CharacterListItem( + type = CharacterType.CAT, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected, + nameResId = R.string.cat_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_cat + ), + CharacterListItem( + type = CharacterType.DOG, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_dog_selected, + nameResId = R.string.dog_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_dog + ), + CharacterListItem( + type = CharacterType.PANDA, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_panda_selected, + nameResId = R.string.panda_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_panda + ), + CharacterListItem( + type = CharacterType.BEAR, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bear_selected, + nameResId = R.string.bear_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_bear + ), + CharacterListItem( + type = CharacterType.BIRD, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bird_selected, + nameResId = R.string.bird_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_bird + ) + ) + } +} diff --git a/feature/profile/src/main/java/com/luckyoct/feature/profile/model/ProfileUiState.kt b/feature/profile/src/main/java/com/luckyoct/feature/profile/model/ProfileUiState.kt index 03f1b26a..3133f867 100644 --- a/feature/profile/src/main/java/com/luckyoct/feature/profile/model/ProfileUiState.kt +++ b/feature/profile/src/main/java/com/luckyoct/feature/profile/model/ProfileUiState.kt @@ -1,8 +1,11 @@ package com.luckyoct.feature.profile.model -sealed interface ProfileEvent { +sealed interface ProfileUiState { - data object Loading : ProfileEvent + data object Loading : ProfileUiState - data object Success : ProfileEvent + data class Success( + val nickname: String, + val characterList: List + ) : ProfileUiState } \ No newline at end of file diff --git a/feature/profile/src/main/res/values/strings.xml b/feature/profile/src/main/res/values/strings.xml index 4443427d..38f15500 100644 --- a/feature/profile/src/main/res/values/strings.xml +++ b/feature/profile/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ 닉네임 입력 1~6자, 한글, 영문 또는 숫자를 입력하세요. 저장하기 + 이미 존재하는 회원 닉네임이에요. 뚝심토끼 포기란없다냥 diff --git a/feature/setting/src/main/java/com/luckyoct/feature/setting/Util.kt b/feature/setting/src/main/java/com/luckyoct/feature/setting/Util.kt new file mode 100644 index 00000000..ba1ad556 --- /dev/null +++ b/feature/setting/src/main/java/com/luckyoct/feature/setting/Util.kt @@ -0,0 +1,22 @@ +package com.luckyoct.feature.setting + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build + +object Util { + fun getAppVersionName(context: Context): String { + return try { + val packageManager = context.packageManager + val packageName = context.packageName + val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) + } else { + packageManager.getPackageInfo(packageName, 0) + } + packageInfo.versionName + } catch (e: Exception) { + "" + } + } +} \ No newline at end of file diff --git a/feature/setting/src/main/java/com/luckyoct/feature/setting/navigation/SettingNavigation.kt b/feature/setting/src/main/java/com/luckyoct/feature/setting/navigation/SettingNavigation.kt index c82f8bdc..e454eaf8 100644 --- a/feature/setting/src/main/java/com/luckyoct/feature/setting/navigation/SettingNavigation.kt +++ b/feature/setting/src/main/java/com/luckyoct/feature/setting/navigation/SettingNavigation.kt @@ -27,7 +27,6 @@ fun NavController.navigateToPrivacyPolicy() { fun NavGraphBuilder.settingNavGraph( onBackClick: () -> Unit, onClickProfileSetting: () -> Unit, - onClickInquiry: () -> Unit, onClickServicePolicy: () -> Unit, onClickPrivacyPolicy: () -> Unit, onClickLogout: () -> Unit @@ -36,7 +35,6 @@ fun NavGraphBuilder.settingNavGraph( SettingRoute( onBackClick = onBackClick, onClickProfileSetting = onClickProfileSetting, - onClickInquiry = onClickInquiry, onClickServicePolicy = onClickServicePolicy, onClickPrivacyPolicy = onClickPrivacyPolicy, onLogout = onClickLogout diff --git a/feature/setting/src/main/java/com/luckyoct/feature/setting/screen/SettingScreen.kt b/feature/setting/src/main/java/com/luckyoct/feature/setting/screen/SettingScreen.kt index 043dfaab..a7434a18 100644 --- a/feature/setting/src/main/java/com/luckyoct/feature/setting/screen/SettingScreen.kt +++ b/feature/setting/src/main/java/com/luckyoct/feature/setting/screen/SettingScreen.kt @@ -21,18 +21,17 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateDialog import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484 @@ -43,13 +42,13 @@ import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateT import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType import com.luckyoct.feature.setting.Event import com.luckyoct.feature.setting.R +import com.luckyoct.feature.setting.Util import kotlinx.coroutines.flow.collectLatest @Composable fun SettingRoute( modifier: Modifier = Modifier, onClickProfileSetting: () -> Unit, - onClickInquiry: () -> Unit, onClickServicePolicy: () -> Unit, onClickPrivacyPolicy: () -> Unit, onBackClick: () -> Unit, @@ -85,7 +84,6 @@ fun SettingRoute( modifier = modifier, onBackClick = { onBackClick() }, onClickProfileSetting = { onClickProfileSetting() }, - onClickInquiry = { onClickInquiry() }, onClickServicePolicy = { onClickServicePolicy() }, onClickPrivacyPolicy = { onClickPrivacyPolicy() }, onClickLogout = { showLogoutDialog.value = true }, @@ -98,7 +96,6 @@ fun SettingScreen( modifier: Modifier = Modifier, onBackClick: () -> Unit, onClickProfileSetting: () -> Unit, - onClickInquiry: () -> Unit, onClickServicePolicy: () -> Unit, onClickPrivacyPolicy: () -> Unit, onClickLogout: () -> Unit, @@ -139,10 +136,28 @@ fun SettingScreen( onClick = { onClickProfileSetting() } ) Divider() + SettingHeader(titleRes = R.string.version_info) + SettingContent( + titleRes = R.string.current_version, + subContent = { + Text( + text = Util.getAppVersionName(LocalContext.current), + style = MissionMateTypography.body_xl_regular, + color = ColorGray1_FF404249 + ) + } + ) + Divider() SettingHeader(titleRes = R.string.help_desk) SettingContent( titleRes = R.string.inquiry, - onClick = { onClickInquiry() } + subContent = { + Text( + text = stringResource(id = R.string.inquiry_email), + style = MissionMateTypography.body_xl_regular, + color = ColorGray1_FF404249 + ) + } ) Divider() SettingHeader(titleRes = R.string.policy) @@ -195,7 +210,11 @@ fun SettingContent( modifier = modifier .fillMaxWidth() .wrapContentHeight() - .clickable { onClick?.invoke() }, + .run { + onClick?.let { + clickable { it() } + } ?: run { this } + }, verticalAlignment = Alignment.CenterVertically ) { Spacer(modifier = modifier.width(24.dp)) @@ -270,7 +289,6 @@ fun SettingScreenPreview() { SettingScreen( onBackClick = {}, onClickProfileSetting = {}, - onClickInquiry = {}, onClickServicePolicy = {}, onClickPrivacyPolicy = {}, onClickLogout = {}, diff --git a/feature/setting/src/main/res/values/strings.xml b/feature/setting/src/main/res/values/strings.xml index 97bce385..ef1de721 100644 --- a/feature/setting/src/main/res/values/strings.xml +++ b/feature/setting/src/main/res/values/strings.xml @@ -18,4 +18,5 @@ 계정탈퇴\n하시겠습니까? 탈퇴하면 저장된 데이터가\n모두 초기화돼요. 탈퇴하기 + missionmateteam@gmail.com \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4bbee1f..efa37d72 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -119,6 +119,7 @@ jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } ## KSP kotlin-ksp = { id = "com.google.devtools.ksp" , version.ref = "ksp"}