diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 45f6e395..7b7faee5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,6 +77,7 @@ dependencies { implementation(project(":feature:login")) implementation(project(":feature:pokitdetail")) implementation(project(":feature:search")) + implementation(project(":feature:settings")) // hilt implementation(libs.hilt) diff --git a/app/src/main/java/pokitmons/pokit/navigation/RootDestination.kt b/app/src/main/java/pokitmons/pokit/navigation/RootDestination.kt index 5c35e25f..59d6396b 100644 --- a/app/src/main/java/pokitmons/pokit/navigation/RootDestination.kt +++ b/app/src/main/java/pokitmons/pokit/navigation/RootDestination.kt @@ -45,3 +45,11 @@ object PokitDetail { object Search { val route: String = "search" } + +object Setting { + val route: String = "setting" +} + +object EditNickname { + val route: String = "editNickname" +} diff --git a/app/src/main/java/pokitmons/pokit/navigation/RootNavHost.kt b/app/src/main/java/pokitmons/pokit/navigation/RootNavHost.kt index e3d66b49..bb817c74 100644 --- a/app/src/main/java/pokitmons/pokit/navigation/RootNavHost.kt +++ b/app/src/main/java/pokitmons/pokit/navigation/RootNavHost.kt @@ -18,6 +18,9 @@ import pokitmons.pokit.LoginViewModel import pokitmons.pokit.login.LoginScreen import pokitmons.pokit.search.SearchScreenContainer import pokitmons.pokit.search.SearchViewModel +import pokitmons.pokit.settings.SettingViewModel +import pokitmons.pokit.settings.nickname.EditNicknameScreen +import pokitmons.pokit.settings.setting.SettingsScreen @Composable fun RootNavHost( @@ -92,5 +95,21 @@ fun RootNavHost( } ) } + + composable(route = Setting.route) { + val viewModel: SettingViewModel = hiltViewModel() + SettingsScreen( + settingViewModel = viewModel, + onNavigateToEditNickname = { navHostController.navigate(EditNickname.route) } + ) + } + + composable(route = EditNickname.route) { + val viewModel: SettingViewModel = hiltViewModel() + EditNicknameScreen( + settingViewModel = viewModel, + onBackPressed = navHostController::popBackStack + ) + } } } diff --git a/data/src/main/java/pokitmons/pokit/data/api/SettingApi.kt b/data/src/main/java/pokitmons/pokit/data/api/SettingApi.kt new file mode 100644 index 00000000..a9140129 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/api/SettingApi.kt @@ -0,0 +1,13 @@ +package pokitmons.pokit.data.api + +import pokitmons.pokit.data.model.setting.reqeust.EditNicknameRequest +import pokitmons.pokit.data.model.setting.response.EditNicknameResponse +import retrofit2.http.Body +import retrofit2.http.PUT + +interface SettingApi { + @PUT("user/nickname") + suspend fun editNickname( + @Body editNicknameRequest: EditNicknameRequest, + ): EditNicknameResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/RemoteSettingDataSourceImpl.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/RemoteSettingDataSourceImpl.kt new file mode 100644 index 00000000..604dbb52 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/RemoteSettingDataSourceImpl.kt @@ -0,0 +1,12 @@ +package pokitmons.pokit.data.datasource.remote.setting + +import pokitmons.pokit.data.api.SettingApi +import pokitmons.pokit.data.model.setting.reqeust.EditNicknameRequest +import pokitmons.pokit.data.model.setting.response.EditNicknameResponse +import javax.inject.Inject + +class RemoteSettingDataSourceImpl @Inject constructor(private val settingApi: SettingApi) : SettingDataSource { + override suspend fun editNickname(editNicknameRequest: EditNicknameRequest): EditNicknameResponse { + return settingApi.editNickname(editNicknameRequest) + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/SettingDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/SettingDataSource.kt new file mode 100644 index 00000000..f9edb38e --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/setting/SettingDataSource.kt @@ -0,0 +1,8 @@ +package pokitmons.pokit.data.datasource.remote.setting + +import pokitmons.pokit.data.model.setting.reqeust.EditNicknameRequest +import pokitmons.pokit.data.model.setting.response.EditNicknameResponse + +interface SettingDataSource { + suspend fun editNickname(editNicknameRequest: EditNicknameRequest): EditNicknameResponse +} diff --git a/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt b/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt index 5d5371cf..052e078a 100644 --- a/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt +++ b/data/src/main/java/pokitmons/pokit/data/di/network/NetworkModule.kt @@ -10,6 +10,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import pokitmons.pokit.data.api.AuthApi +import pokitmons.pokit.data.api.SettingApi import retrofit2.Retrofit import java.util.concurrent.TimeUnit import javax.inject.Singleton @@ -67,4 +68,9 @@ object NetworkModule { fun provideAuthService(retrofit: Retrofit): AuthApi { return retrofit.create(AuthApi::class.java) } + + @Provides + fun provideSettingService(retrofit: Retrofit): SettingApi { + return retrofit.create(SettingApi::class.java) + } } diff --git a/data/src/main/java/pokitmons/pokit/data/di/setting/SettingModule.kt b/data/src/main/java/pokitmons/pokit/data/di/setting/SettingModule.kt new file mode 100644 index 00000000..ec050258 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/di/setting/SettingModule.kt @@ -0,0 +1,23 @@ +package pokitmons.pokit.data.di.setting + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import pokitmons.pokit.data.datasource.remote.setting.RemoteSettingDataSourceImpl +import pokitmons.pokit.data.datasource.remote.setting.SettingDataSource +import pokitmons.pokit.data.repository.setting.SettingRepositoryImpl +import pokitmons.pokit.domain.repository.setting.SettingRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class SettingModule { + @Binds + @Singleton + abstract fun bindSettingRepository(settingRepositoryImpl: SettingRepositoryImpl): SettingRepository + + @Binds + @Singleton + abstract fun bindSettingDataSource(settingDataSourceImpl: RemoteSettingDataSourceImpl): SettingDataSource +} diff --git a/data/src/main/java/pokitmons/pokit/data/mapper/setting/SettingMapper.kt b/data/src/main/java/pokitmons/pokit/data/mapper/setting/SettingMapper.kt new file mode 100644 index 00000000..3080207b --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/mapper/setting/SettingMapper.kt @@ -0,0 +1,14 @@ +package pokitmons.pokit.data.mapper.setting + +import pokitmons.pokit.data.model.setting.response.EditNicknameResponse +import pokitmons.pokit.domain.model.setting.EditNicknameResult + +object SettingMapper { + fun mapperToEditNickname(editNicknameResponse: EditNicknameResponse): EditNicknameResult { + return EditNicknameResult( + id = editNicknameResponse.id, + nickname = editNicknameResponse.nickname, + email = editNicknameResponse.email + ) + } +} diff --git a/data/src/main/java/pokitmons/pokit/data/model/setting/reqeust/EditNicknameRequest.kt b/data/src/main/java/pokitmons/pokit/data/model/setting/reqeust/EditNicknameRequest.kt new file mode 100644 index 00000000..edfa5e55 --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/setting/reqeust/EditNicknameRequest.kt @@ -0,0 +1,8 @@ +package pokitmons.pokit.data.model.setting.reqeust + +import kotlinx.serialization.Serializable + +@Serializable +data class EditNicknameRequest( + val nickname: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/model/setting/response/EditNicknameResponse.kt b/data/src/main/java/pokitmons/pokit/data/model/setting/response/EditNicknameResponse.kt new file mode 100644 index 00000000..75fdd27e --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/model/setting/response/EditNicknameResponse.kt @@ -0,0 +1,10 @@ +package pokitmons.pokit.data.model.setting.response + +import kotlinx.serialization.Serializable + +@Serializable +data class EditNicknameResponse( + val email: String, + val id: Int, + val nickname: String, +) diff --git a/data/src/main/java/pokitmons/pokit/data/repository/setting/SettingRepositoryImpl.kt b/data/src/main/java/pokitmons/pokit/data/repository/setting/SettingRepositoryImpl.kt new file mode 100644 index 00000000..aea7447f --- /dev/null +++ b/data/src/main/java/pokitmons/pokit/data/repository/setting/SettingRepositoryImpl.kt @@ -0,0 +1,25 @@ +package pokitmons.pokit.data.repository.setting + +import pokitmons.pokit.data.datasource.remote.setting.SettingDataSource +import pokitmons.pokit.data.mapper.setting.SettingMapper +import pokitmons.pokit.data.model.common.parseErrorResult +import pokitmons.pokit.data.model.setting.reqeust.EditNicknameRequest +import pokitmons.pokit.data.model.setting.response.EditNicknameResponse +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.setting.EditNicknameResult +import pokitmons.pokit.domain.repository.setting.SettingRepository +import javax.inject.Inject + +class SettingRepositoryImpl @Inject constructor( + private val remoteSettingDataSource: SettingDataSource, +) : SettingRepository { + override suspend fun editNickname(nickname: String): PokitResult { + return runCatching { + val editNicknameResponse: EditNicknameResponse = remoteSettingDataSource.editNickname(EditNicknameRequest(nickname)) + val editNicknameMapper = SettingMapper.mapperToEditNickname(editNicknameResponse) + PokitResult.Success(editNicknameMapper) + }.getOrElse { throwable -> + parseErrorResult(throwable) + } + } +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/setting/EditNicknameResult.kt b/domain/src/main/java/pokitmons/pokit/domain/model/setting/EditNicknameResult.kt new file mode 100644 index 00000000..09119047 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/setting/EditNicknameResult.kt @@ -0,0 +1,7 @@ +package pokitmons.pokit.domain.model.setting + +data class EditNicknameResult( + val email: String, + val id: Int, + val nickname: String, +) diff --git a/domain/src/main/java/pokitmons/pokit/domain/repository/setting/SettingRepository.kt b/domain/src/main/java/pokitmons/pokit/domain/repository/setting/SettingRepository.kt new file mode 100644 index 00000000..6cdd03ac --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/repository/setting/SettingRepository.kt @@ -0,0 +1,8 @@ +package pokitmons.pokit.domain.repository.setting + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.setting.EditNicknameResult + +interface SettingRepository { + suspend fun editNickname(nickname: String): PokitResult +} diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/setting/EditNicknameUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/setting/EditNicknameUseCase.kt new file mode 100644 index 00000000..b439e285 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/setting/EditNicknameUseCase.kt @@ -0,0 +1,15 @@ +package pokitmons.pokit.domain.usecase.setting + +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.setting.EditNicknameResult +import pokitmons.pokit.domain.repository.setting.SettingRepository +import javax.inject.Inject + +class EditNicknameUseCase @Inject constructor(private val settingRepository: SettingRepository) { + suspend fun editNickname(nickname: String): PokitResult { + return when (val editNicknameResult = settingRepository.editNickname(nickname)) { + is PokitResult.Success -> PokitResult.Success(editNicknameResult.result) + is PokitResult.Error -> PokitResult.Error(editNicknameResult.error) + } + } +} diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts new file mode 100644 index 00000000..ff2bb5e9 --- /dev/null +++ b/feature/settings/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + alias(libs.plugins.com.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.hilt) + id("kotlin-kapt") +} + +android { + namespace = "pokitmons.pokit.settings" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + implementation(libs.androidx.navigation.compose) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) + + implementation(project(":core:ui")) + implementation(project(":domain")) + implementation(project(":feature:login")) + + // hilt + implementation(libs.hilt) + kapt(libs.hilt.compiler) + + // navigation + implementation(libs.androidx.navigation.compose) + implementation(libs.hilt.navigation.compose) +} diff --git a/feature/settings/proguard-rules.pro b/feature/settings/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/feature/settings/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/settings/src/androidTest/java/pokitmons/pokit/settings/ExampleInstrumentedTest.kt b/feature/settings/src/androidTest/java/pokitmons/pokit/settings/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..69e8d671 --- /dev/null +++ b/feature/settings/src/androidTest/java/pokitmons/pokit/settings/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package pokitmons.pokit.settings + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.TestCase.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * 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("pokitmons.pokit.settings.test", appContext.packageName) + } +} diff --git a/feature/settings/src/main/AndroidManifest.xml b/feature/settings/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/settings/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/SettingViewModel.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/SettingViewModel.kt new file mode 100644 index 00000000..1c07a18b --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/SettingViewModel.kt @@ -0,0 +1,80 @@ +package pokitmons.pokit.settings + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.auth.InputNicknameUseCase +import pokitmons.pokit.domain.usecase.setting.EditNicknameUseCase +import pokitmons.pokit.model.DuplicateNicknameState +import javax.inject.Inject + +@HiltViewModel +class SettingViewModel @Inject constructor( + private val nicknameUseCase: InputNicknameUseCase, + private val editNicknameUseCase: EditNicknameUseCase, +) : ViewModel() { + private var duplicateNicknameJob: Job? = null + + private val _isBottomSheetVisible: MutableState = mutableStateOf(false) + val isBottomSheetVisible: Boolean + get() = _isBottomSheetVisible.value + + private val _inputNicknameState = MutableStateFlow(DuplicateNicknameState()) + val inputNicknameState: StateFlow + get() = _inputNicknameState.asStateFlow() + + fun changeBottomSheetHideState() { + _isBottomSheetVisible.value = !_isBottomSheetVisible.value + } + + fun inputText(inputNickname: String) { + _inputNicknameState.update { duplicateNicknameState -> + duplicateNicknameState.copy(nickname = inputNickname) + } + } + + fun checkDuplicateNickname(nickname: String) { + duplicateNicknameJob?.cancel() + duplicateNicknameJob = viewModelScope.launch { + delay(1.second()) + when (val duplicateNicknameResult = nicknameUseCase.checkDuplicateNickname(nickname)) { + is PokitResult.Success -> { + _inputNicknameState.update { duplicateNicknameState -> + duplicateNicknameState.copy(isDuplicate = duplicateNicknameResult.result.isDuplicate) + } + } + + is PokitResult.Error -> {} + } + } + } + + fun editNickname() { + viewModelScope.launch { + when (val editNicknameResult = editNicknameUseCase.editNickname(_inputNicknameState.value.nickname)) { + is PokitResult.Success -> { + } + + is PokitResult.Error -> { + } + } + } + } + + // TODO 확장함수 모듈 생성하기 + companion object { + private fun Int.second(): Long { + return (this * 1000L) + } + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/EditNicknameScreen.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/EditNicknameScreen.kt new file mode 100644 index 00000000..1347258b --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/EditNicknameScreen.kt @@ -0,0 +1,74 @@ +package pokitmons.pokit.settings.nickname + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.atom.button.PokitButton +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize +import pokitmons.pokit.core.ui.components.block.labeledinput.LabeledInput +import pokitmons.pokit.settings.SettingViewModel +import pokitmons.pokit.settings.R.string as StringResource + +private const val NICKNAME_MAX_LENGTH = 10 // TODO 매직넘버를 포함하는 모듈화 추가 후 마이그레이션 예정 + +@Composable +fun EditNicknameScreen( + settingViewModel: SettingViewModel, + onBackPressed: () -> Unit, +) { + val inputNicknameState by settingViewModel.inputNicknameState.collectAsState() + + Box( + modifier = Modifier + .padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 28.dp) + .fillMaxSize() + ) { + Column { + NicknameHeader(onBackPressed) + LabeledInput( + modifier = Modifier + .fillMaxWidth() + .imePadding(), + label = "", + inputText = inputNicknameState.nickname, + maxLength = NICKNAME_MAX_LENGTH, + sub = when { + inputNicknameState.nickname.length < NICKNAME_MAX_LENGTH -> stringResource(id = StringResource.input_restriction_message) + !inputNicknameState.isDuplicate -> stringResource(id = StringResource.nickname_already_in_use) + else -> stringResource(id = StringResource.input_max_length) + }, + isError = inputNicknameState.nickname.length > NICKNAME_MAX_LENGTH || !inputNicknameState.isDuplicate, + hintText = stringResource(id = StringResource.input_nickname_hint), + onChangeText = { text -> + if (text.length <= NICKNAME_MAX_LENGTH) { + settingViewModel.apply { + inputText(text) + checkDuplicateNickname(text) + } + } + } + ) + } + + PokitButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + text = stringResource(id = StringResource.save), + icon = null, + size = PokitButtonSize.LARGE, + enable = !inputNicknameState.isDuplicate, + onClick = { settingViewModel.editNickname() } + ) + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/NicknameHeader.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/NicknameHeader.kt new file mode 100644 index 00000000..7e21063d --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/nickname/NicknameHeader.kt @@ -0,0 +1,42 @@ +package pokitmons.pokit.settings.nickname + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.core.ui.R.drawable as DrawableResource +import pokitmons.pokit.settings.R.string as StringResource + +@Composable +fun NicknameHeader(onBackPressed: () -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + ) { + Icon( + painter = painterResource(id = DrawableResource.icon_24_arrow_left), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterStart) + .clickable { onBackPressed() } + .size(24.dp) + ) + + Text( + text = stringResource(StringResource.edit_nickname_title), + style = PokitTheme.typography.title3, + modifier = Modifier.align(Alignment.Center) + ) + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/setting/DividerItem.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/DividerItem.kt new file mode 100644 index 00000000..f1e96d3d --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/DividerItem.kt @@ -0,0 +1,25 @@ +package pokitmons.pokit.settings.setting + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun DividerItem() { + Column(modifier = Modifier.fillMaxWidth()) { + Spacer(modifier = Modifier.height(16.dp)) + Spacer( + modifier = Modifier + .height(6.dp) + .fillMaxWidth() + .background(color = PokitTheme.colors.backgroundPrimary) + ) + Spacer(modifier = Modifier.height(16.dp)) + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingHeader.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingHeader.kt new file mode 100644 index 00000000..aeaa575b --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingHeader.kt @@ -0,0 +1,44 @@ +package pokitmons.pokit.settings.setting + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +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.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme +import pokitmons.pokit.core.ui.R.drawable as DrawableResource +import pokitmons.pokit.settings.R.string as StringResource + +@Composable +fun SettingHeader() { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .padding(start = 24.dp, end = 24.dp) + ) { + Icon( + painter = painterResource(id = DrawableResource.icon_24_arrow_left), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterStart) + .clickable { } + .size(24.dp) + ) + + Text( + text = stringResource(StringResource.settings), + style = PokitTheme.typography.title3, + modifier = Modifier.align(Alignment.Center) + ) + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingItem.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingItem.kt new file mode 100644 index 00000000..151a0aba --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingItem.kt @@ -0,0 +1,54 @@ +package pokitmons.pokit.settings.setting + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun SettingItem( + title: String, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .padding(start = 24.dp, end = 24.dp) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + onClick() + }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = PokitTheme.typography.title3 + ) + + Spacer(modifier = Modifier.weight(1f)) + + Icon( + painter = painterResource(id = R.drawable.icon_24_arrow_right), + contentDescription = null, + modifier = Modifier + .size(24.dp) + ) + } +} diff --git a/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingsScreen.kt b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingsScreen.kt new file mode 100644 index 00000000..750dddcb --- /dev/null +++ b/feature/settings/src/main/java/pokitmons/pokit/settings/setting/SettingsScreen.kt @@ -0,0 +1,65 @@ +package pokitmons.pokit.settings.setting + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet +import pokitmons.pokit.core.ui.components.template.removeItemBottomSheet.TwoButtonBottomSheetContent +import pokitmons.pokit.settings.SettingViewModel +import pokitmons.pokit.settings.R.string as StringResource + +@Composable +fun SettingsScreen( + settingViewModel: SettingViewModel, + onNavigateToEditNickname: () -> Unit, +) { + Column(modifier = Modifier.fillMaxWidth()) { + SettingHeader() + Spacer(modifier = Modifier.height(16.dp)) + Column(modifier = Modifier.fillMaxWidth()) { + SettingItem(title = stringResource(StringResource.nickname_settings)) { + onNavigateToEditNickname() + } + SettingItem(title = stringResource(StringResource.notification_settings)) { + } + + DividerItem() + + SettingItem(title = stringResource(StringResource.announcements)) { + } + SettingItem(title = stringResource(StringResource.terms_of_service)) { + } + SettingItem(title = stringResource(StringResource.privacy_policy)) { + } + SettingItem(title = stringResource(StringResource.customer_support)) { + } + + DividerItem() + + SettingItem(title = stringResource(StringResource.logout)) { + settingViewModel.changeBottomSheetHideState() + } + SettingItem(title = stringResource(StringResource.delete_account)) { + settingViewModel.changeBottomSheetHideState() + } + } + } + + // TODO 회원탈퇴 분기 + PokitBottomSheet( + onHideBottomSheet = { }, + show = settingViewModel.isBottomSheetVisible + ) { + TwoButtonBottomSheetContent( + title = stringResource(id = StringResource.logout_title), + rightButtonText = stringResource(id = StringResource.logout), + onClickLeftButton = { settingViewModel.changeBottomSheetHideState() }, + onClickRightButton = {} + ) + } +} diff --git a/feature/settings/src/main/res/values/strings.xml b/feature/settings/src/main/res/values/strings.xml new file mode 100644 index 00000000..d6d42fce --- /dev/null +++ b/feature/settings/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + 설정 + 닉네임 설정 + 알림 설정 + 공지사항 + 서비스 이용약관 + 개인정보 처리방침 + 고객문의 + 로그아웃 + 회원탈퇴 + 로그아웃하시겠습니까? + 회원 탈퇴하시겠습니까? + 함께 저장된 모든 정보가 삭제되며, 복구하실 수 없습니다. + 닉네임 설정 + 저장 + 한글, 영어, 숫자로만 입력이 가능합니다. + 사용 중인 닉네임입니다. + 최대 20자까지 입력 가능합니다. + 닉네임을 입력해주세요 + \ No newline at end of file diff --git a/feature/settings/src/test/java/pokitmons/pokit/settings/ExampleUnitTest.kt b/feature/settings/src/test/java/pokitmons/pokit/settings/ExampleUnitTest.kt new file mode 100644 index 00000000..df53e25c --- /dev/null +++ b/feature/settings/src/test/java/pokitmons/pokit/settings/ExampleUnitTest.kt @@ -0,0 +1,16 @@ +package pokitmons.pokit.settings + +import junit.framework.TestCase.assertEquals +import org.junit.Test + +/** + * 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) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ff5a1ddd..4c53243d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,3 +30,4 @@ include(":feature:addpokit") include(":feature:login") include(":feature:pokitdetail") include(":feature:search") +include(":feature:settings")