diff --git a/build-logic/convention/src/main/java/in/koreatech/convention/AndoriodHilt.kt b/build-logic/convention/src/main/java/in/koreatech/convention/AndoriodHilt.kt index a3107d041..babb805fa 100644 --- a/build-logic/convention/src/main/java/in/koreatech/convention/AndoriodHilt.kt +++ b/build-logic/convention/src/main/java/in/koreatech/convention/AndoriodHilt.kt @@ -8,6 +8,7 @@ internal fun Project.configureAndroidHilt( commonExtension: CommonExtension<*, *, *, *>, ){ dependencies { + implementation(libs.findBundle("hilt").get()) kapt(libs.findLibrary("hilt-compiler").get()) } diff --git a/build-logic/convention/src/main/java/in/koreatech/convention/AndroidProject.kt b/build-logic/convention/src/main/java/in/koreatech/convention/AndroidProject.kt index 2a92fa487..70d95f58c 100644 --- a/build-logic/convention/src/main/java/in/koreatech/convention/AndroidProject.kt +++ b/build-logic/convention/src/main/java/in/koreatech/convention/AndroidProject.kt @@ -1,5 +1,6 @@ package `in`.koreatech.convention +import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.CommonExtension import org.gradle.api.JavaVersion import org.gradle.api.plugins.ExtensionAware @@ -9,12 +10,17 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions internal fun configureAndroidProject( commonExtension: CommonExtension<*, *, *, *>, ) { + (commonExtension as? ApplicationExtension)?.let { + it.defaultConfig.targetSdk = 34 + } + commonExtension.apply { compileSdk = 34 - + (this as? ApplicationExtension)?.let { + it.defaultConfig.targetSdk = 34 + } defaultConfig { minSdk = 23 - testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } @@ -28,6 +34,7 @@ internal fun configureAndroidProject( jvmTarget = JavaVersion.VERSION_11.toString() } } + } fun CommonExtension<*, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) { diff --git a/business/build.gradle.kts b/business/build.gradle.kts index bfb4d30fd..cd09af40d 100644 --- a/business/build.gradle.kts +++ b/business/build.gradle.kts @@ -11,8 +11,10 @@ plugins { android { namespace = "in.koreatech.business" - packagingOptions { - resources.pickFirsts.add("META-INF/gradle/incremental.annotation.processors") + androidComponents { + onVariants(selector().withBuildType("release")) { + it.packaging.resources.excludes.add("META-INF/**") + } } // 2 files found with path 'META-INF/gradle/incremental.annotation.processors' from inputs: 오류 해결 @@ -34,6 +36,10 @@ dependencies { implementation(libs.bundles.compose) implementation(libs.lifecycle.runtime.ktx) implementation(libs.compose.navigation) + implementation(libs.androidx.security.crypto) + implementation(libs.coil) + implementation(libs.coil.compose) + implementation(libs.androidx.security.crypto) implementation(project(mapOf("path" to ":domain"))) implementation(project(mapOf("path" to ":data"))) implementation(project(mapOf("path" to ":core"))) diff --git a/business/src/main/java/in/koreatech/business/BusinessMainActivity.kt b/business/src/main/java/in/koreatech/business/BusinessMainActivity.kt index ce32c3d3d..abcb2a8b8 100644 --- a/business/src/main/java/in/koreatech/business/BusinessMainActivity.kt +++ b/business/src/main/java/in/koreatech/business/BusinessMainActivity.kt @@ -12,8 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import dagger.hilt.android.AndroidEntryPoint -import `in`.koreatech.business.feature.signup.navigator.SignupNavigator -import `in`.koreatech.business.feature_changepassword.navigator.ChangePassword +import `in`.koreatech.business.feature.insertstore.navigator.InsertStoreNavigator import `in`.koreatech.business.ui.theme.KOIN_ANDROIDTheme @AndroidEntryPoint @@ -27,8 +26,7 @@ class BusinessMainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background ) { - ChangePassword() - //SignupNavigator(modifier = Modifier.fillMaxSize()) + InsertStoreNavigator() } } } diff --git a/business/src/main/java/in/koreatech/business/di/network/AuthNetworkModule.kt b/business/src/main/java/in/koreatech/business/di/network/AuthNetworkModule.kt index 5eab72ff9..1eb12fcf0 100644 --- a/business/src/main/java/in/koreatech/business/di/network/AuthNetworkModule.kt +++ b/business/src/main/java/in/koreatech/business/di/network/AuthNetworkModule.kt @@ -6,10 +6,15 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import `in`.koreatech.business.util.OwnerTokenAuthenticator import `in`.koreatech.business.util.RefreshTokenInterceptor import `in`.koreatech.koin.core.qualifier.Auth +import `in`.koreatech.koin.core.qualifier.OwnerAuth +import `in`.koreatech.koin.core.qualifier.PreSignedUrl import `in`.koreatech.koin.core.qualifier.Refresh import `in`.koreatech.koin.core.qualifier.ServerUrl +import `in`.koreatech.koin.data.api.PreSignedUrlApi +import `in`.koreatech.koin.data.api.UploadUrlApi import `in`.koreatech.koin.data.api.UserApi import `in`.koreatech.koin.data.api.auth.UserAuthApi import `in`.koreatech.koin.data.source.local.TokenLocalDataSource @@ -90,7 +95,122 @@ object AuthNetworkModule { @Singleton fun provideUserAuthApi( @Auth retrofit: Retrofit - ) : UserAuthApi { + ): UserAuthApi { return retrofit.create(UserAuthApi::class.java) } + +} + + +@Module +@InstallIn(SingletonComponent::class) +object BusinessAuthNetworkModule { + @OwnerAuth + @Provides + @Singleton + fun provideOwnerAuthInterceptor( + tokenLocalDataSource: TokenLocalDataSource + ): Interceptor { + return Interceptor { chain: Interceptor.Chain -> + runBlocking { + val ownerAccessToken = tokenLocalDataSource.getOwnerAccessToken() ?: "" + val newRequest: Request = chain.request().newBuilder() + .addHeader("Authorization", "Bearer $ownerAccessToken") + .build() + chain.proceed(newRequest) + } + } + } + + @OwnerAuth + @Provides + @Singleton + fun provideTokenAuthenticator( + @ApplicationContext applicationContext: Context, + tokenLocalDataSource: TokenLocalDataSource + ) = OwnerTokenAuthenticator(applicationContext, tokenLocalDataSource) + + @OwnerAuth + @Provides + @Singleton + fun provideOwnerAuthOkHttpClient( + httpLoggingInterceptor: HttpLoggingInterceptor, + @OwnerAuth ownerAuthInterceptor: Interceptor, + @OwnerAuth tokenAuthenticator: OwnerTokenAuthenticator + ): OkHttpClient { + return OkHttpClient.Builder().apply { + connectTimeout(10, TimeUnit.SECONDS) + readTimeout(30, TimeUnit.SECONDS) + writeTimeout(15, TimeUnit.SECONDS) + addInterceptor(httpLoggingInterceptor) + addInterceptor(ownerAuthInterceptor) + authenticator(tokenAuthenticator) + }.build() + } + + @OwnerAuth + @Provides + @Singleton + fun provideOwnerAuthRetrofit( + @ServerUrl baseUrl: String, + @OwnerAuth ownerOkHttpClient: OkHttpClient + ): Retrofit { + return Retrofit.Builder() + .client(ownerOkHttpClient) + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + @Provides + @Singleton + fun provideUploadUrlApi( + @OwnerAuth retrofit: Retrofit + ): UploadUrlApi { + return retrofit.create(UploadUrlApi::class.java) + } +} + + + + +@Module +@InstallIn(SingletonComponent::class) +object PreSignedUrlNetworkModule { + @PreSignedUrl + @Provides + @Singleton + fun provideOwnerAuthOkHttpClient( + httpLoggingInterceptor: HttpLoggingInterceptor + ): OkHttpClient { + return OkHttpClient.Builder().apply { + connectTimeout(10, TimeUnit.SECONDS) + readTimeout(30, TimeUnit.SECONDS) + writeTimeout(15, TimeUnit.SECONDS) + addInterceptor(httpLoggingInterceptor) + }.build() + } + + @Provides + @Singleton + @PreSignedUrl + fun providePreSignedUrlRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl("https://kap-test.s3.ap-northeast-2.amazonaws.com/") + .client( + OkHttpClient.Builder().addInterceptor( + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.HEADERS + } + ).build() + ).build() + } + + @Provides + @Singleton + fun provideUploadUrlApi( + @PreSignedUrl retrofit: Retrofit + ): PreSignedUrlApi { + return retrofit.create(PreSignedUrlApi::class.java) + } } \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreen.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreen.kt new file mode 100644 index 000000000..6f4a7293f --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreen.kt @@ -0,0 +1,44 @@ +package `in`.koreatech.business.feature.insertstore.insertdetailinfo + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import `in`.koreatech.koin.domain.model.owner.insertstore.StoreBasicInfo +import org.orbitmvi.orbit.compose.collectAsState + +@Composable +fun InsertDetailInfoScreen( + modifier: Modifier = Modifier, + onBackPress: () -> Unit, + stroeBasicInfo: StoreBasicInfo = StoreBasicInfo(), + viewModel: InsertDetailInfoScreenViewModel = hiltViewModel() +) { + val state = viewModel.collectAsState().value + InsertDetailInfoScreenImpl( + storeNumber = state.storeAddress + ) +} + + +@Composable +fun InsertDetailInfoScreenImpl( + modifier: Modifier = Modifier, + storeNumber: String = "" +) { + Column( + modifier = modifier + ) { + Text( + text = storeNumber + ) + } +} + +@Preview +@Composable +fun PreviewInsertDetailInfoScreen(){ + InsertDetailInfoScreenImpl() +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreenSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreenSideEffect.kt new file mode 100644 index 000000000..326af0dc9 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreenSideEffect.kt @@ -0,0 +1,16 @@ +package `in`.koreatech.business.feature.insertstore.insertdetailinfo + +import `in`.koreatech.business.feature.insertstore.insertmaininfo.InsertBasicInfoScreenSideEffect + +sealed class InsertDetailInfoScreenSideEffect { + + data class ShowMessage(val type: DetailInfoErrorType): InsertDetailInfoScreenSideEffect() + + data class NavigateToInsertDetailInfoScreen(val storeBasicInfo: InsertDetailInfoScreenSideEffect) : InsertDetailInfoScreenSideEffect() +} + +enum class DetailInfoErrorType { + NullStoreName, + NullStoreAddress, + NullStoreImage +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreenState.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreenState.kt new file mode 100644 index 000000000..8c87bfb75 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreenState.kt @@ -0,0 +1,13 @@ +package `in`.koreatech.business.feature.insertstore.insertdetailinfo + +import android.net.Uri +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class InsertDetailInfoScreenState ( + val storeCategory: Int = -1, + val storeName: String = "", + val storeAddress: String = "", + val storeImage: Uri = Uri.EMPTY +): Parcelable \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreenViewmodel.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreenViewmodel.kt new file mode 100644 index 000000000..37cb372ea --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/InsertDetailInfoScreenViewmodel.kt @@ -0,0 +1,44 @@ +package `in`.koreatech.business.feature.insertstore.insertdetailinfo + +import android.net.Uri +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.google.gson.Gson +import dagger.hilt.android.lifecycle.HiltViewModel +import `in`.koreatech.business.feature.insertstore.insertmaininfo.InsertBasicInfoScreenState +import `in`.koreatech.koin.domain.model.owner.insertstore.StoreBasicInfo +import kotlinx.android.parcel.Parcelize +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class InsertDetailInfoScreenViewModel @Inject constructor( + savedStateHandle: SavedStateHandle +): ViewModel(), ContainerHost { + override val container: Container = + container(InsertDetailInfoScreenState(), savedStateHandle = savedStateHandle) { + + val storeBasicInfoJson: InsertBasicInfoScreenState? = savedStateHandle.get("storeBasicInfo") + checkNotNull(storeBasicInfoJson) + getStoreBasicInfo(storeBasicInfoJson) + } + + + private fun getStoreBasicInfo(storeBasicInfo: InsertBasicInfoScreenState){ + intent{ + reduce { + state.copy( + storeCategory = storeBasicInfo.storeCategory, + storeName = storeBasicInfo.storeName, + storeAddress = storeBasicInfo.storeAddress, + storeImage = storeBasicInfo.storeImage + ) + } + } + } +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreen.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreen.kt new file mode 100644 index 000000000..b22aa4170 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreen.kt @@ -0,0 +1,302 @@ +package `in`.koreatech.business.feature.insertstore.insertmaininfo + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.provider.MediaStore +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.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.width +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.rememberAsyncImagePainter +import coil.compose.rememberImagePainter +import `in`.koreatech.business.feature.insertstore.selectcategory.InsertStoreProgressBar +import `in`.koreatech.business.ui.theme.ColorActiveButton +import `in`.koreatech.business.ui.theme.ColorDisabledButton +import `in`.koreatech.business.ui.theme.ColorMinor +import `in`.koreatech.business.ui.theme.ColorPrimary +import `in`.koreatech.koin.core.R +import `in`.koreatech.koin.core.toast.ToastUtil +import `in`.koreatech.koin.domain.model.owner.insertstore.StoreBasicInfo +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect + + +@Composable +fun InsertBasicInfoScreen( + onBackPressed: () -> Unit, + navigateToInsertDetailInfoScreen: (InsertBasicInfoScreenState) -> Unit, + viewModel: InsertBasicInfoScreenViewModel = hiltViewModel() +) { + val state = viewModel.collectAsState().value + + InsertBasicInfoScreenImpl( + storeImage = state.storeImage, + storeName = state.storeName, + storeAddress = state.storeAddress, + storeImageIsEmpty = state.storeImageIsEmpty, + isBasicInfoValid = state.isBasicInfoValid, + onStoreImageChange = { + viewModel.insertStoreImage(it) + }, + onStoreNameChange = { + viewModel.insertStoreName(it) + }, + onStoreAddressChange = { + viewModel.insertStoreAddress(it) + }, + onNextButtonClicked = { + viewModel.onNextButtonClick() + }, + onBackPressed = onBackPressed + ) + + HandleSideEffects(viewModel, navigateToInsertDetailInfoScreen) +} + +@Composable +fun InsertBasicInfoScreenImpl( + modifier: Modifier = Modifier, + storeImage: Uri = Uri.EMPTY, + storeImageIsEmpty: Boolean = true, + storeName: String = "", + storeAddress: String = "", + isBasicInfoValid: Boolean = false, + onStoreImageChange: (Uri) -> Unit = {}, + onStoreNameChange: (String) -> Unit = {}, + onStoreAddressChange: (String) -> Unit = {}, + onNextButtonClicked: () -> Unit = {}, + onBackPressed: () -> Unit = {} +) { + + val galleryLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { + onStoreImageChange(it) + } + } + } + + Column( + modifier = modifier.fillMaxSize() + ) { + Box( + modifier = modifier + .padding(top = 56.dp, start = 10.dp, bottom = 18.dp) + .size(40.dp) + .clickable { + onBackPressed() + } + ) { + Image( + painter = painterResource(R.drawable.ic_arrow_left), + contentDescription = "backArrow", + modifier = modifier + .size(40.dp) + ) + } + + Text( + modifier = modifier.padding(top = 35.dp, start = 40.dp), + text = stringResource(id = R.string.insert_store), + fontSize = 24.sp + ) + + Text( + modifier = modifier.padding(top = 34.dp, start = 32.dp), + text = stringResource(id = R.string.insert_store_main_info), + fontSize = 18.sp + ) + + InsertStoreProgressBar(modifier, 0.50f, R.string.insert_store_basic_info, R.string.page_two) + + + Box( + modifier = Modifier + .padding(top = 32.dp) + .padding(horizontal = 32.dp) + .fillMaxWidth() + .height(200.dp) + .border(width = 1.dp, color = ColorMinor) + .clickable { + galleryLauncher.launch(takePhotoFromAlbumIntent) + } + , + contentAlignment = Alignment.Center + ){ + if(storeImageIsEmpty){ + Image( + modifier = Modifier + .padding(horizontal = 71.dp, vertical = 53.dp), + painter = painterResource(R.drawable.ic_no_store_image), + contentDescription = "enptyStoreImage" + ) + } + else{ + Image( + modifier = Modifier.fillMaxSize(), + painter = rememberAsyncImagePainter( + storeImage + ), + contentDescription = "storeImage", + contentScale = ContentScale.FillBounds + ) + } + } + + NameTextField( + stringResource(id = R.string.insert_store_store_name), + storeName, + onStoreNameChange, + 32.dp + ) + + NameTextField( + stringResource(id = R.string.insert_store_store_address), + storeAddress, + onStoreAddressChange, + 24.dp + ) + + Button( + onClick = onNextButtonClicked, + colors = if(isBasicInfoValid) ButtonDefaults.buttonColors(ColorPrimary) + else ButtonDefaults.buttonColors(ColorDisabledButton), + shape = RectangleShape, + modifier = modifier + .padding(top = 57.dp, start = 240.dp, end = 16.dp) + .height(38.dp) + .width(105.dp) + ) { + Text( + text = stringResource(id = R.string.next), + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + + } + +} + +@Composable +private fun HandleSideEffects(viewModel: InsertBasicInfoScreenViewModel, navigateToInsertDetailInfoScreen: (InsertBasicInfoScreenState) -> Unit) { + val context = LocalContext.current + + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + is InsertBasicInfoScreenSideEffect.NavigateToInsertDetailInfoScreen -> navigateToInsertDetailInfoScreen(sideEffect.storeBasicInfo) + is InsertBasicInfoScreenSideEffect.ShowMessage -> { + val message = when (sideEffect.type) { + BasicInfoErrorType.NullStoreName -> context.getString(R.string.insert_store_null_store_name) + BasicInfoErrorType.NullStoreAddress -> context.getString(R.string.insert_store_null_store_address) + BasicInfoErrorType.NullStoreImage -> context.getString(R.string.insert_store_null_store_image) + } + ToastUtil.getInstance().makeShort(message) + } + } + } +} + +@Composable +fun NameTextField( + textString: String = "", + inputString: String = "", + onStringChange: (String) -> Unit = {}, + paddingTopValue: Dp = 10.dp +){ + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp) + .padding(top = paddingTopValue), + verticalAlignment = Alignment.CenterVertically + ){ + Text( + text = textString, + fontSize = 14.sp, + color = ColorActiveButton, + fontWeight = FontWeight.Bold + ) + + BorderTextField(inputString, onStringChange) + } +} + +@Composable +fun BorderTextField( + inputString: String = "", + onStringChange: (String) -> Unit = {}, +){ + Box( + modifier = Modifier + .padding(start = 30.dp) + .border(width = 1.dp, color = ColorMinor) + .height(37.dp), + contentAlignment = Alignment.CenterStart + ) { + BasicTextField( + value = inputString, + onValueChange = onStringChange, + textStyle = TextStyle( + color = Color.Black, + fontSize = 14.sp + ), + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp) + ) + } +} + +private val takePhotoFromAlbumIntent = + Intent(Intent.ACTION_GET_CONTENT, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply { + type = "image/*" + action = Intent.ACTION_GET_CONTENT + putExtra( + Intent.EXTRA_MIME_TYPES, + arrayOf("image/jpeg", "image/png", "image/bmp", "image/webp") + ) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) + } + + + +@Preview +@Composable +fun PreviewInsertBasicInfo() { + InsertBasicInfoScreenImpl() +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreenSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreenSideEffect.kt new file mode 100644 index 000000000..6a19873ca --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreenSideEffect.kt @@ -0,0 +1,16 @@ +package `in`.koreatech.business.feature.insertstore.insertmaininfo + +import `in`.koreatech.koin.domain.model.owner.insertstore.StoreBasicInfo + + +sealed class InsertBasicInfoScreenSideEffect { + + data class ShowMessage(val type: BasicInfoErrorType): InsertBasicInfoScreenSideEffect() + data class NavigateToInsertDetailInfoScreen(val storeBasicInfo: InsertBasicInfoScreenState) : InsertBasicInfoScreenSideEffect() +} + +enum class BasicInfoErrorType { + NullStoreName, + NullStoreAddress, + NullStoreImage +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreenState.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreenState.kt new file mode 100644 index 000000000..f3eda5515 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreenState.kt @@ -0,0 +1,15 @@ +package `in`.koreatech.business.feature.insertstore.insertmaininfo + +import android.net.Uri +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class InsertBasicInfoScreenState( + val storeName: String = "", + val storeAddress: String = "", + val storeImage: Uri = Uri.EMPTY, + val storeImageIsEmpty: Boolean = true, + val storeCategory: Int = 0, + val isBasicInfoValid: Boolean = false +): Parcelable \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreenViewModel.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreenViewModel.kt new file mode 100644 index 000000000..088271b79 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/insertmaininfo/InsertBasicInfoScreenViewModel.kt @@ -0,0 +1,96 @@ +package `in`.koreatech.business.feature.insertstore.insertmaininfo + +import android.net.Uri +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import `in`.koreatech.koin.domain.model.owner.insertstore.StoreBasicInfo +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class InsertBasicInfoScreenViewModel @Inject constructor( + savedStateHandle: SavedStateHandle +): ViewModel(), ContainerHost { + override val container: Container = + container(InsertBasicInfoScreenState(), savedStateHandle = savedStateHandle){ + val categoryId = savedStateHandle.get("categoryId") + checkNotNull(categoryId) + getCategoryId(categoryId) + } + + fun insertStoreName(storeName: String) = intent { + reduce { + state.copy(storeName = storeName) + } + isBasicInfoValidate() + } + + fun insertStoreAddress(storeAddress: String) = intent{ + reduce { + state.copy(storeAddress = storeAddress) + } + isBasicInfoValidate() + } + + fun insertStoreImage(storeImage: Uri) = intent{ + reduce { + state.copy(storeImage = storeImage) + } + isBasicInfoValidate() + storeImageIsEmpty() + } + + fun onNextButtonClick(){ + intent{ + if(state.isBasicInfoValid){ + val storeBasicInfo = InsertBasicInfoScreenState( + storeName = state.storeName, + storeAddress = state.storeAddress, + storeImage = state.storeImage, + storeCategory = state.storeCategory + ) + postSideEffect(InsertBasicInfoScreenSideEffect.NavigateToInsertDetailInfoScreen(storeBasicInfo)) + return@intent + } + + when { + state.storeImageIsEmpty -> postSideEffect(InsertBasicInfoScreenSideEffect.ShowMessage(BasicInfoErrorType.NullStoreImage)) + state.storeName.isEmpty() -> postSideEffect(InsertBasicInfoScreenSideEffect.ShowMessage(BasicInfoErrorType.NullStoreName)) + state.storeAddress.isEmpty() -> postSideEffect(InsertBasicInfoScreenSideEffect.ShowMessage(BasicInfoErrorType.NullStoreAddress)) + } + } + } + + private fun storeImageIsEmpty() = intent{ + reduce { + state.copy(storeImageIsEmpty = state.storeImage == Uri.EMPTY) + } + isBasicInfoValidate() + } + + private fun getCategoryId(id: Int){ + intent{ + reduce { + state.copy( + storeCategory = id + ) + } + } + } + + private fun isBasicInfoValidate() = intent { + reduce { + state.copy( + isBasicInfoValid = state.storeAddress.isNotBlank() && state.storeName.isNotBlank() && state.storeImage != Uri.EMPTY + ) + } + } + +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/navigator/InsertStoreRoute.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/navigator/InsertStoreRoute.kt new file mode 100644 index 000000000..09b17b8b3 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/navigator/InsertStoreRoute.kt @@ -0,0 +1,8 @@ +package `in`.koreatech.business.feature.insertstore.navigator + +enum class InsertStoreRoute(){ + START, + SELECT_CATEGORY, + BASIC_INFO, + DETAIL_INFO +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/navigator/InsertStoreRouteNavigator.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/navigator/InsertStoreRouteNavigator.kt new file mode 100644 index 000000000..60a8962dc --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/navigator/InsertStoreRouteNavigator.kt @@ -0,0 +1,120 @@ +package `in`.koreatech.business.feature.insertstore.navigator + +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import androidx.navigation.NavOptions +import androidx.navigation.NavType +import androidx.navigation.Navigator +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.google.gson.Gson +import `in`.koreatech.business.feature.insertstore.insertdetailinfo.InsertDetailInfoScreen +import `in`.koreatech.business.feature.insertstore.insertmaininfo.InsertBasicInfoScreen +import `in`.koreatech.business.feature.insertstore.insertmaininfo.InsertBasicInfoScreenState +import `in`.koreatech.business.feature.insertstore.selectcategory.SelectCategoryScreen +import `in`.koreatech.business.feature.insertstore.startinsetstore.StartInsertScreen +import `in`.koreatech.koin.domain.model.owner.insertstore.StoreBasicInfo + +@Composable +fun InsertStoreNavigator( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController() +) { + NavHost( + navController = navController, + startDestination = InsertStoreRoute.START.name, + modifier = modifier + ) { + + composable( + route = InsertStoreRoute.START.name, + ) { + StartInsertScreen( + goToSelectCategoryScreen = { + navController.navigate(InsertStoreRoute.SELECT_CATEGORY.name) + }, + onBackPressed = { + navController.navigateUp() + } + ) + } + + composable( + route = InsertStoreRoute.SELECT_CATEGORY.name, + ) { + SelectCategoryScreen( + navigateToInsertBasicInfoScreen = { + navigateToMainInfo(navController, it) + }, + onBackPressed = { + navController.navigateUp() + } + ) + } + + composable( + route = "${InsertStoreRoute.BASIC_INFO.name}/{categoryId}", + arguments = listOf( + navArgument("categoryId"){ + type = NavType.IntType + defaultValue = 0 + } + ) + ){ + InsertBasicInfoScreen( + onBackPressed = { + navController.navigateUp() + }, + navigateToInsertDetailInfoScreen = { + navigateToDetailInfo(navController, it) + } + ) + } + + composable( + route = InsertStoreRoute.DETAIL_INFO.name + ){ + InsertDetailInfoScreen( + onBackPress = { + navController.navigateUp() + } + ) + } + } +} + +private fun navigateToMainInfo( + navController: NavController, + categoryId: Int +) { + navController.navigate("${InsertStoreRoute.BASIC_INFO}/${categoryId}") +} + +private fun navigateToDetailInfo( + navController: NavController, + storeBasicInfo: InsertBasicInfoScreenState +) { + val bundle = Bundle() + bundle.putParcelable("storeBasicInfo", storeBasicInfo) + navController.navigate(InsertStoreRoute.DETAIL_INFO.name, args = bundle) +} + + +fun NavController.navigate( + route: String, + args: Bundle, + navOptions: NavOptions? = null, + navigatorExtras: Navigator.Extras? = null +) { + val nodeId = graph.findNode(route = route)?.id + if (nodeId != null) { + navigate(nodeId, args, navOptions, navigatorExtras) + } +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreen.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreen.kt new file mode 100644 index 000000000..f25c25152 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreen.kt @@ -0,0 +1,300 @@ +package `in`.koreatech.business.feature.insertstore.selectcategory + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +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.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.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.ProgressBarRangeInfo +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.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.rememberAsyncImagePainter +import coil.compose.rememberImagePainter +import coil.request.ImageRequest +import coil.transform.CircleCropTransformation +import `in`.koreatech.business.ui.theme.ColorDisabledButton +import `in`.koreatech.business.ui.theme.ColorPrimary +import `in`.koreatech.business.ui.theme.ColorSecondary +import `in`.koreatech.koin.core.R +import `in`.koreatech.koin.core.toast.ToastUtil +import `in`.koreatech.koin.domain.model.store.StoreCategories +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect + +@Composable +fun SelectCategoryScreen( + modifier: Modifier = Modifier, + navigateToInsertBasicInfoScreen: (Int) -> Unit, + onBackPressed: () -> Unit, + viewModel: SelectCategoryScreenViewModel = hiltViewModel() +) { + val state = viewModel.collectAsState().value + SelectCategoryScreenImpl( + modifier = modifier, + categories = state.categories, + categoryId = state.categoryId, + chooseCategory = { + viewModel.chooseCategory(it) + }, + categoryIdIsValid = state.categoryIdIsValid, + nextButtonClicked = { + viewModel.goToInsertBasicInfoScreen() + }, + onBackPressed = onBackPressed + ) + + HandleSideEffects(viewModel, state.categoryId, navigateToInsertBasicInfoScreen) +} + + + +@Composable +fun SelectCategoryScreenImpl( + modifier: Modifier = Modifier, + categories: List = emptyList(), + categoryId: Int = -1, + categoryIdIsValid: Boolean = true, + chooseCategory: (Int) -> Unit = {}, + nextButtonClicked: () -> Unit = {}, + onBackPressed: () -> Unit = {} +) { + Column( + modifier = modifier.fillMaxSize() + ) { + Box( + modifier = Modifier + .padding(top = 56.dp, start = 10.dp, bottom = 18.dp) + .width(40.dp) + .height(40.dp) + .clickable { onBackPressed() } + ) { + Image( + painter = painterResource(R.drawable.ic_arrow_left), + contentDescription = "backArrow", + modifier = modifier + .width(40.dp) + .height(40.dp) + ) + } + + Text( + modifier = Modifier.padding(top = 35.dp, start = 40.dp), + text = stringResource(id = R.string.insert_store), + fontSize = 24.sp + ) + + Text( + modifier = Modifier.padding(top = 34.dp, start = 32.dp), + text = stringResource(id = R.string.insert_store_main_info), + fontSize = 18.sp + ) + + InsertStoreProgressBar(Modifier, 0.25f, R.string.insert_store_category_setting, R.string.page_one) + + Text( + modifier = Modifier.padding(top = 24.dp, start = 40.dp), + text = stringResource(id = R.string.insert_store_choose_category), + fontSize = 18.sp + ) + + LazyHorizontalGrid( + rows = GridCells.Fixed(2), + modifier = Modifier + .padding(top = 28.dp) + .padding(horizontal = 15.dp) + .height(200.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + items(categories) { category -> + CategoryItem( + modifier = Modifier.clickable { + chooseCategory(category.id) + }, + imageUrl = category.imageUrl, + name = category.name, + categoryId = category.id, + choosedCategoryId = categoryId + ) + } + } + + Button( + onClick = nextButtonClicked, + colors = if(categoryIdIsValid)ButtonDefaults.buttonColors(ColorPrimary) else ButtonDefaults.buttonColors(ColorDisabledButton), + shape = RectangleShape, + modifier = modifier + .padding(top = 57.dp, start = 240.dp, end = 16.dp) + .height(38.dp) + .width(105.dp) + ) { + Text( + text = stringResource(id = R.string.next), + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + + } + +} + +@Composable +fun InsertStoreProgressBar( + modifier: Modifier, + range: Float, + resourceId: Int, + pageId: Int +) { + Row( + modifier = modifier + .padding(top = 24.dp) + .padding(horizontal = 32.dp) + ) { + Text( + text = stringResource(id = resourceId), + fontSize = 15.sp, + color = ColorSecondary + ) + + Text( + modifier = Modifier.padding(start = 160.dp), + text = stringResource(id = pageId), + fontSize = 15.sp, + color = ColorSecondary + ) + + } + LinearProgressIndicator( + modifier = Modifier + .padding(top = 4.dp) + .padding(horizontal = 32.dp) + .fillMaxWidth() + .height(3.dp) + , + color = ColorSecondary, + backgroundColor = Color.Gray, + progress = range + ) +} + +@Composable +private fun HandleSideEffects( + viewModel: SelectCategoryScreenViewModel, + categoryId: Int, + navigateToInsertMainInfoScreen: (categoryId: Int) -> Unit +) { + val context = LocalContext.current + + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + is SelectCategoryScreenSideEffect.NavigateToInsertBasicInfoScreen -> navigateToInsertMainInfoScreen(categoryId) + is SelectCategoryScreenSideEffect.NotSelectCategory -> ToastUtil.getInstance().makeShort(context.getString(R.string.insert_store_choose_category)) + } + } +} +@Composable +fun CategoryItem( + modifier: Modifier, + imageUrl: String, + name: String, + categoryId: Int, + choosedCategoryId: Int +) { + Column( + modifier = modifier + .width(70.dp) + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ){ + Image( + modifier = modifier + .size(44.dp) + .border( + width = 2.dp, + color = if (categoryId == choosedCategoryId) ColorSecondary else Color.Transparent, + shape = CircleShape + ) + , + painter = rememberAsyncImagePainter( + ImageRequest.Builder(LocalContext.current).data(data = imageUrl) + .apply(block = fun ImageRequest.Builder.() { + crossfade(true) + transformations(CircleCropTransformation()) + }).build() + ), + alignment = Alignment.Center, + contentDescription = "category_image" + ) + Text( + modifier = modifier.fillMaxWidth(), + text = name, + textAlign = TextAlign.Center, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = if(categoryId == choosedCategoryId) ColorSecondary else Color.Black + ) + } +} + +@Preview +@Composable +fun PreviewCategoryItem() { + CategoryItem( + modifier = Modifier, + imageUrl = "", + name = "치킨", + categoryId = 0, + choosedCategoryId = 0 + ) +} + +@Preview +@Composable +fun PreviewSelectCategoryScreen(){ + SelectCategoryScreenImpl( + categories = sampleCategories + ) +} + +val sampleCategories = listOf( + StoreCategories(1,"imageUrl1", "일반음식점"), + StoreCategories(1, "imageUrl2", "패스트푸드"), + StoreCategories(1,"imageUrl3", "카페"), + StoreCategories(1,"imageUrl4", "디저트"), + StoreCategories(1,"imageUrl5", "바베큐"), + StoreCategories(1,"imageUrl5", "바베큐"), + StoreCategories(1,"imageUrl5", "바베큐"), + StoreCategories(1,"imageUrl5", "바베큐"), + StoreCategories(1,"imageUrl5", "바베큐") +) \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreenSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreenSideEffect.kt new file mode 100644 index 000000000..5d530d463 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreenSideEffect.kt @@ -0,0 +1,9 @@ +package `in`.koreatech.business.feature.insertstore.selectcategory + +import `in`.koreatech.business.feature_changepassword.passwordauthentication.ErrorType +import `in`.koreatech.business.feature_changepassword.passwordauthentication.PasswordAuthenticationSideEffect + +sealed class SelectCategoryScreenSideEffect { + data class NavigateToInsertBasicInfoScreen(val categoryId: Int): SelectCategoryScreenSideEffect() + object NotSelectCategory: SelectCategoryScreenSideEffect() +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreenState.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreenState.kt new file mode 100644 index 000000000..99e9598da --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreenState.kt @@ -0,0 +1,9 @@ +package `in`.koreatech.business.feature.insertstore.selectcategory + +import `in`.koreatech.koin.domain.model.store.StoreCategories + +data class SelectCategoryScreenState( + val categories: List = emptyList(), + val categoryId: Int = -1, + val categoryIdIsValid: Boolean = false +) \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreenViewModel.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreenViewModel.kt new file mode 100644 index 000000000..9f3783364 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/selectcategory/SelectCategoryScreenViewModel.kt @@ -0,0 +1,70 @@ +package `in`.koreatech.business.feature.insertstore.selectcategory + +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import `in`.koreatech.koin.domain.usecase.business.OwnerChangePasswordUseCase +import `in`.koreatech.koin.domain.usecase.store.GetStoreCategoriesUseCase +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + + +@HiltViewModel +class SelectCategoryScreenViewModel @Inject constructor( + private val getStoreCategoriesUseCase: GetStoreCategoriesUseCase +) : ViewModel(), ContainerHost{ + override val container = container(SelectCategoryScreenState()) + + + init { + getCategory() + } + + fun chooseCategory(categoryId: Int) = intent{ + reduce{ + state.copy( + categoryId = categoryId + ) + } + categoryIdIsValid() + } + + fun goToInsertBasicInfoScreen(){ + intent { + if(state.categoryIdIsValid){ + postSideEffect(SelectCategoryScreenSideEffect.NavigateToInsertBasicInfoScreen(state.categoryId)) + } + else{ + postSideEffect(SelectCategoryScreenSideEffect.NotSelectCategory) + } + } + } + private fun categoryIdIsValid(){ + intent{ + reduce{ + state.copy( + categoryIdIsValid = (state.categoryId != -1) + ) + } + } + } + private fun getCategory() { + intent { + viewModelScope.launch { + val categories = getStoreCategoriesUseCase().drop(1) + reduce { + state.copy( + categories = categories + ) + } + } + } + } +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/startinsetstore/StartInsertStore.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/startinsetstore/StartInsertStore.kt new file mode 100644 index 000000000..5c9cb8854 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/startinsetstore/StartInsertStore.kt @@ -0,0 +1,121 @@ +package `in`.koreatech.business.feature.insertstore.startinsetstore + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +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.unit.sp +import `in`.koreatech.koin.core.R +import `in`.koreatech.business.ui.theme.Blue1 +import `in`.koreatech.business.ui.theme.ColorPrimary + +@Composable +fun StartInsertScreen( + modifier: Modifier = Modifier, + goToSelectCategoryScreen: () -> Unit, + onBackPressed: () -> Unit +) { + Column( + modifier = modifier.fillMaxSize() + ) { + Box( + modifier = modifier + .padding(top = 56.dp, start = 10.dp , bottom = 18.dp) + .width(40.dp) + .height(40.dp) + .clickable { onBackPressed } + + ) { + Image( + painter = painterResource(R.drawable.ic_arrow_left), + contentDescription = "backArrow", + modifier = modifier + .width(40.dp) + .height(40.dp) + .clickable { } + ) + } + + Image( + painter = painterResource(id = R.drawable.ic_edit), + contentDescription = "finish_mark", + alignment = Alignment.Center, + modifier = modifier + .fillMaxWidth() + .padding(top = 103.dp, bottom = 30.dp) + .height(55.dp) + .width(55.dp) + ) + + Text( + text = stringResource(R.string.insert_store_info), + fontSize = 24.sp, + color = ColorPrimary, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 46.dp) + .padding(bottom = 16.dp) + ) + + Text( + text = stringResource(R.string.insert_store_guide), + fontSize = 16.sp, + color = Blue1, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 46.dp) + .padding(bottom = 51.dp) + ) + + Button( + onClick = goToSelectCategoryScreen, + colors = ButtonDefaults.buttonColors(ColorPrimary), + shape = RectangleShape, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 33.dp) + .height(44.dp) + ) { + Text( + text = stringResource(id = R.string.insert_store_start), + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + } +} + + +@Preview +@Composable +fun PreviewStartInsertScreen(){ + StartInsertScreen( + modifier = Modifier, + goToSelectCategoryScreen = {} , + onBackPressed = {} + ) +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/accountauth/AccountAuthScreen.kt b/business/src/main/java/in/koreatech/business/feature/signup/accountauth/AccountAuthScreen.kt deleted file mode 100644 index edd54857b..000000000 --- a/business/src/main/java/in/koreatech/business/feature/signup/accountauth/AccountAuthScreen.kt +++ /dev/null @@ -1,199 +0,0 @@ -package `in`.koreatech.business.feature.signup.accountauth - - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.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.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight.Companion.Bold -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import `in`.koreatech.business.R -import `in`.koreatech.business.feature.textfield.AuthTextField -import `in`.koreatech.business.ui.theme.ColorDescription -import `in`.koreatech.business.ui.theme.ColorSecondary -import `in`.koreatech.business.ui.theme.ColorUnarchived -import kotlinx.coroutines.delay - -@Composable -fun AccountAuthScreen( - modifier: Modifier = Modifier, - email: String, - onBackClicked: () -> Unit = {}, - onNextClicked: () -> Unit = {}, -) { - var authCode by remember { mutableStateOf("") } - Column( - modifier = modifier, - ) { - IconButton( - modifier = Modifier.padding(vertical = 24.dp), - onClick = { onBackClicked() } - ) { - Icon( - modifier = Modifier.padding(start = 10.dp), - painter = painterResource(id = R.drawable.ic_arrow_back), - contentDescription = stringResource(id = R.string.back_icon), - ) - } - Column( - modifier = Modifier - .padding(horizontal = 32.dp), - verticalArrangement = Arrangement.Center, - ) { - Text( - text = stringResource(id = R.string.master_sign_up), - fontSize = 24.sp, - fontWeight = Bold, - ) - Spacer(modifier = Modifier.height(40.dp)) - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - modifier = Modifier, - color = ColorSecondary, - text = stringResource(id = R.string.account_authentication) - ) - Text(text = stringResource(id = R.string.two_third)) - } - Canvas( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - drawLine( - color = ColorUnarchived, - start = Offset(0f - 40, 0f), - end = Offset(size.width + 35, size.height), - strokeWidth = 4.dp.toPx(), - cap = StrokeCap.Round - ) - drawLine( - color = ColorSecondary, - start = Offset(0f - 40, 0f), - end = Offset((size.width + 40) * 2 / 3, size.height), - strokeWidth = 4.dp.toPx(), - cap = StrokeCap.Round - ) - } - Spacer(modifier = Modifier.height(32.dp)) - Row { - Text( - text = email, - fontSize = 15.sp, - color = ColorSecondary, - ) - Text( - text = stringResource(id = R.string.by), - fontSize = 15.sp, - color = ColorDescription, - ) - } - Text( - text = stringResource(id = R.string.verification_code_prompt), - fontSize = 15.sp, - color = ColorDescription, - ) - Spacer(modifier = Modifier.height(38.dp)) - AuthTextField( - value = authCode, - onValueChange = { authCode = it }, - modifier = Modifier.fillMaxWidth(), - label = stringResource(id = R.string.enter_verification_code), - textStyle = TextStyle.Default.copy(fontSize = 20.sp), - isPassword = true, - - ) - Spacer(modifier = Modifier.height(8.dp)) - CountdownTimer() - Spacer(modifier = Modifier.height(183.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - - Button(modifier = Modifier - .width(141.dp) - .height(44.dp), - shape = RectangleShape, - colors = ButtonDefaults.buttonColors( - contentColor = Color.White, - disabledContentColor = Color.White, - ), - onClick = { } - ) { - Text(text = stringResource(id = R.string.auth_code_resend)) - } - Spacer(modifier = Modifier.width(14.dp)) - - Button(modifier = Modifier - .width(141.dp) - .height(44.dp), - shape = RectangleShape, - colors = ButtonDefaults.buttonColors( - contentColor = Color.White, - disabledContentColor = Color.White, - ), - onClick = { - }) { - Text(text = stringResource(id = R.string.next)) - } - } - } - } -} - - -@Composable -fun CountdownTimer() { - - var timeLeft by remember { mutableStateOf(300) } - var minutes by remember { mutableStateOf(0) } - var seconds by remember { mutableStateOf(0) } - - LaunchedEffect(key1 = timeLeft) { - while (timeLeft > 0) { - delay(1000L) - timeLeft-- - minutes = timeLeft / 60 - seconds = timeLeft % 60 - } - } - - val formattedTimeLeft = "%02d".format(minutes) - val formattedSeconds = "%02d".format(seconds) - - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = stringResource(id = R.string.time_limit) + "$formattedTimeLeft : $formattedSeconds" - ) -} diff --git a/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountAuthSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountAuthSideEffect.kt deleted file mode 100644 index b7ecf8133..000000000 --- a/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountAuthSideEffect.kt +++ /dev/null @@ -1,6 +0,0 @@ -package `in`.koreatech.business.feature.signup.accountsetup - -sealed class AccountAuthSideEffect { - data object NavigateToBackScreen : AccountAuthSideEffect() - data class NavigateToNextScreen(val email: String) : AccountAuthSideEffect() -} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountAuthState.kt b/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountAuthState.kt deleted file mode 100644 index 7ec787d9d..000000000 --- a/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountAuthState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package `in`.koreatech.business.feature.signup.accountsetup - -import `in`.koreatech.koin.domain.state.signup.SignupContinuationState - -data class AccountAuthState( - val id: String = "", - val password: String = "", - val passwordConfirm: String = "", - val email: String = "", - val signupContinuationState: SignupContinuationState = SignupContinuationState.RequestedEmailValidation, - val signUpContinuationError:Throwable? = null, - val isLoading: Boolean = false -){ - val isPasswordEnabled: Boolean - get() = id.isNotEmpty() && password.isNotEmpty() && passwordConfirm.isNotEmpty() && email.isNotEmpty() -} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupScreen.kt b/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupScreen.kt index 3ed6b5133..63d08599e 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupScreen.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupScreen.kt @@ -2,6 +2,7 @@ package `in`.koreatech.business.feature.signup.accountsetup import androidx.compose.foundation.Canvas 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 @@ -9,12 +10,17 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color @@ -31,12 +37,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import `in`.koreatech.business.R import `in`.koreatech.business.feature.textfield.LinedTextField import `in`.koreatech.business.ui.theme.ColorPrimary -import `in`.koreatech.business.ui.theme.ColorDisabledButton -import `in`.koreatech.business.ui.theme.ColorSecondary -import `in`.koreatech.business.ui.theme.ColorHelper import `in`.koreatech.business.ui.theme.ColorUnarchived +import `in`.koreatech.business.ui.theme.Gray1 +import `in`.koreatech.business.ui.theme.Gray2 import `in`.koreatech.business.ui.theme.KOIN_ANDROIDTheme -import `in`.koreatech.koin.domain.state.signup.SignupContinuationState import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -45,141 +49,255 @@ fun AccountSetupScreen( modifier: Modifier = Modifier, viewModel: AccountSetupViewModel = hiltViewModel(), onBackClicked: () -> Unit = {}, - onNextClicked: (String) -> Unit = {}, + onNextClicked: () -> Unit = {}, ) { + val scrollState = rememberScrollState() val state = viewModel.collectAsState().value Column( modifier = modifier.fillMaxSize(), ) { - IconButton( - modifier = Modifier.padding(vertical = 24.dp), - onClick = { onBackClicked() } - ) { - Icon( - modifier = Modifier.padding(start = 10.dp), - painter = painterResource(id = R.drawable.ic_arrow_back), - contentDescription = stringResource(id = R.string.back_icon), - ) + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + ) { + IconButton( + onClick = { viewModel.onBackButtonClicked() }, + modifier = Modifier.align(Alignment.CenterStart) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back), + contentDescription = stringResource(id = R.string.back_icon), + ) + } + + Text( + text = stringResource(id = R.string.sign_up), + fontSize = 18.sp, + fontWeight = Bold, + modifier = Modifier.align(Alignment.Center) + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Column( + modifier = Modifier + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center, + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier, + color = ColorPrimary, + fontWeight = Bold, + text = stringResource(id = R.string.input_basic_information) + ) + Text( + text = stringResource(id = R.string.two_third), + color = ColorPrimary, + fontWeight = Bold, + ) + } + + Canvas( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + drawLine( + color = ColorUnarchived, + start = Offset(-40f, 0f), + end = Offset(size.width + 40, size.height), + strokeWidth = 4.dp.toPx(), + cap = StrokeCap.Round + ) + drawLine( + color = ColorPrimary, + start = Offset(-40f, 0f), + end = Offset((size.width + 35) / 3 * 2, size.height), + strokeWidth = 4.dp.toPx(), + cap = StrokeCap.Round + ) + } + } } + Spacer(modifier = Modifier.height(20.dp)) + Column( modifier = Modifier - .padding(horizontal = 32.dp), + .padding(horizontal = 24.dp).verticalScroll(scrollState), verticalArrangement = Arrangement.Center, ) { - Text( - text = stringResource(id = R.string.master_sign_up), - fontSize = 24.sp, - fontWeight = Bold, - ) - Spacer(modifier = Modifier.height(40.dp)) + + Spacer(modifier = Modifier.height(10.dp)) + + Text(text = stringResource(id = R.string.id), fontSize = 14.sp, fontWeight = Bold) Row( modifier = Modifier .fillMaxWidth(), + verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.SpaceBetween ) { - Text( - modifier = Modifier, - color = ColorSecondary, - text = stringResource(id = R.string.input_basic_information), - ) - Text(text = stringResource(id = R.string.one_third), color = ColorSecondary) - } - Canvas( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - drawLine( - color = ColorUnarchived, - start = Offset(-40f, 0f), - end = Offset(size.width + 35, size.height), - strokeWidth = 4.dp.toPx(), - cap = StrokeCap.Round + LinedTextField( + value = state.id, + onValueChange = { viewModel.onIdChanged(it) }, + modifier = Modifier + .width(203.dp), + label = stringResource(id = R.string.enter_id), + errorText = stringResource(id = R.string.id_not_validate), + textStyle = TextStyle.Default.copy(fontSize = 15.sp), ) - drawLine( - color = ColorSecondary, - start = Offset(-40f, 0f), - end = Offset((size.width + 40) / 3, size.height), - strokeWidth = 4.dp.toPx(), - cap = StrokeCap.Round - ) - } - Spacer(modifier = Modifier.height(32.dp)) - - LinedTextField( - value = state.id, - onValueChange = { viewModel.onIdChanged(it) }, - modifier = Modifier.fillMaxWidth(), - label = stringResource(id = R.string.id), - textStyle = TextStyle.Default.copy(fontSize = 15.sp), - lineColor = ColorHelper, - ) + Button(modifier = Modifier + .width(115.dp) + .height(41.dp), + shape = RoundedCornerShape(4.dp), + enabled = state.id.isNotEmpty(), + colors = ButtonDefaults.buttonColors( + backgroundColor = ColorPrimary, + contentColor = Color.White, + disabledBackgroundColor = Gray2, + disabledContentColor = Gray1, + ), + onClick = { + TODO("아이디 중복 확인") + }) { + Text( + text = stringResource(id = R.string.check_duplicate), + fontSize = 13.sp, + fontWeight = Bold, + ) + } + } + Spacer(modifier = Modifier.height(10.dp)) + Text(text = stringResource(id = R.string.password), fontSize = 14.sp, fontWeight = Bold) LinedTextField( value = state.password, onValueChange = { viewModel.onPasswordChanged(it) }, modifier = Modifier.fillMaxWidth(), - label = stringResource(id = R.string.password), + label = stringResource(id = R.string.enter_password), textStyle = TextStyle.Default.copy(fontSize = 15.sp), - lineColor = ColorHelper, + errorText = stringResource(id = R.string.password_not_validate), isPassword = true, - helperText = stringResource(id = R.string.password_requirements), + isError = state.isPasswordError, ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(id = R.string.password_confirm), + fontSize = 14.sp, + fontWeight = Bold + ) LinedTextField( value = state.passwordConfirm, onValueChange = { viewModel.onPasswordConfirmChanged(it) }, modifier = Modifier.fillMaxWidth(), - label = stringResource(id = R.string.password_confirm), + label = stringResource(id = R.string.enter_password_confirm), textStyle = TextStyle.Default.copy(fontSize = 15.sp), - lineColor = ColorHelper, isPassword = true, errorText = stringResource(id = R.string.password_mismatch), - isError = state.signupContinuationState == SignupContinuationState.PasswordNotMatching, + isError = state.isPasswordConfirmError, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = stringResource(id = R.string.phone_number), + fontSize = 14.sp, + fontWeight = Bold ) LinedTextField( - value = state.email, - onValueChange = { viewModel.onEmailChanged(it) }, + value = state.phoneNumber, + onValueChange = { viewModel.onPhoneNumChanged(it) }, modifier = Modifier.fillMaxWidth(), - label = stringResource(id = R.string.email_confirm), + label = stringResource(id = R.string.enter_phone_number), textStyle = TextStyle.Default.copy(fontSize = 15.sp), - lineColor = ColorHelper, - errorText = stringResource(id = R.string.email_not_validate), - isError = state.signupContinuationState == SignupContinuationState.EmailIsNotValidate, + errorText = stringResource(id = R.string.phone_number_not_validate), + isError = state.isPhoneNumberError, ) + Spacer(modifier = Modifier.height(10.dp)) - Spacer(modifier = Modifier.height(78.dp)) + Text( + text = stringResource(id = R.string.authentication_code), + fontSize = 14.sp, + fontWeight = Bold + ) + + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceBetween + ) { + LinedTextField( + modifier = Modifier + .width(203.dp), + value = state.authCode, + onValueChange = { viewModel.onAuthCodeChanged(it) }, + label = stringResource(id = R.string.enter_verification_code), + textStyle = TextStyle.Default.copy(fontSize = 20.sp), + errorText = stringResource(id = R.string.sms_code_not_validate), + isError = state.signUpContinuationError != null, + ) + + Button(modifier = Modifier + .width(115.dp) + .height(41.dp), + shape = RoundedCornerShape(4.dp), + enabled = state.phoneNumber.isNotEmpty(), + colors = ButtonDefaults.buttonColors( + backgroundColor = ColorPrimary, + contentColor = Color.White, + disabledBackgroundColor = Gray2, + disabledContentColor = Gray1, + ), + onClick = { + viewModel.sendSmsVerificationCode( + state.phoneNumber + ) + }) { + Text( + text = stringResource(id = R.string.send_authentication_code), + fontWeight = Bold, + fontSize = 13.sp, + ) + } + } + + Spacer(modifier = Modifier.height(55.dp)) Button(modifier = Modifier .fillMaxWidth() .height(44.dp), shape = RectangleShape, - enabled = state.isPasswordEnabled, + enabled = state.isButtonEnabled, colors = ButtonDefaults.buttonColors( backgroundColor = ColorPrimary, contentColor = Color.White, - disabledBackgroundColor = ColorDisabledButton, - disabledContentColor = Color.White, + disabledBackgroundColor = Gray2, + disabledContentColor = Gray1, ), onClick = { - viewModel.checkInfo( - state.email, + viewModel.verifySmsCode( state.password, state.passwordConfirm, - ) - if (state.signupContinuationState == SignupContinuationState.CheckComplete) - viewModel.onNextButtonClicked() - viewModel.postEmailVerification( - state.email, + state.phoneNumber, + state.authCode, ) }) { Text( - text = stringResource(id = R.string.email_authentication), - fontSize = 15.sp, - color = Color.White, + text = stringResource(id = R.string.next), + fontSize = 13.sp, + fontWeight = Bold, ) } } @@ -187,8 +305,8 @@ fun AccountSetupScreen( viewModel.collectSideEffect { when (it) { - is AccountAuthSideEffect.NavigateToNextScreen -> onNextClicked(it.email) - AccountAuthSideEffect.NavigateToBackScreen -> onBackClicked() + is AccountSetupSideEffect.NavigateToNextScreen -> onNextClicked() + AccountSetupSideEffect.NavigateToBackScreen -> onBackClicked() } } } @@ -201,5 +319,5 @@ fun AccountSetupScreenPreview() { onNextClicked = {}, onBackClicked = {} ) - } + } } diff --git a/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupSideEffect.kt new file mode 100644 index 000000000..9572e8c3c --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupSideEffect.kt @@ -0,0 +1,6 @@ +package `in`.koreatech.business.feature.signup.accountsetup + +sealed class AccountSetupSideEffect { + data object NavigateToBackScreen : AccountSetupSideEffect() + data class NavigateToNextScreen(val email:String) : AccountSetupSideEffect() +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupState.kt b/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupState.kt new file mode 100644 index 000000000..0b4b34500 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupState.kt @@ -0,0 +1,18 @@ +package `in`.koreatech.business.feature.signup.accountsetup + +import `in`.koreatech.koin.domain.state.signup.SignupContinuationState + +data class AccountSetupState( + val id: String = "", + val password: String = "", + val passwordConfirm: String = "", + val phoneNumber: String = "", + val authCode: String = "", + val isPasswordError: Boolean = false, + val isPasswordConfirmError: Boolean = false, + val isPhoneNumberError: Boolean = false, + val signupContinuationState: SignupContinuationState = SignupContinuationState.RequestedEmailValidation, + val signUpContinuationError: Throwable? = null, + val isLoading: Boolean = false, + val isButtonEnabled: Boolean = false +) \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupViewModel.kt b/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupViewModel.kt index 6f3b2a2c4..0e63cd979 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupViewModel.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/accountsetup/AccountSetupViewModel.kt @@ -1,10 +1,24 @@ package `in`.koreatech.business.feature.signup.accountsetup +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.internal.composableLambdaInstance +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import `in`.koreatech.koin.domain.usecase.business.SendSignupEmailUseCase +import `in`.koreatech.koin.domain.usecase.business.BusinessSignupCheckUseCase +import `in`.koreatech.koin.domain.usecase.business.SendSignupSmsCodeUseCase +import `in`.koreatech.koin.domain.util.ext.isValidPassword import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent @@ -15,9 +29,53 @@ import javax.inject.Inject @HiltViewModel class AccountSetupViewModel @Inject constructor( - private val sendSignupEmailUseCase: SendSignupEmailUseCase, -) : ViewModel(), ContainerHost { - override val container = container(AccountAuthState()) + private val sendSignupSmsCodeUseCase: SendSignupSmsCodeUseCase, + private val businessSignupCheckUseCase: BusinessSignupCheckUseCase, +) : ViewModel(), ContainerHost { + override val container = + container(AccountSetupState()) + + + private val passwordFlow = container.stateFlow + .map { it.password } + .distinctUntilChanged() + + private val idFlow = container.stateFlow + .map { it.id } + .distinctUntilChanged() + + private val passwordConfirmFlow = container.stateFlow + .map { it.passwordConfirm } + .distinctUntilChanged() + + private val phoneNumberFlow = container.stateFlow + .map { it.phoneNumber } + .distinctUntilChanged() + + private val authCodeFlow = container.stateFlow + .map { it.authCode } + .distinctUntilChanged() + + init { + combine( + passwordFlow, + idFlow, + passwordConfirmFlow, + phoneNumberFlow, + authCodeFlow + ) { password, id, passwordConfirm, phoneNumber, authCode -> + password.isNotEmpty() && id.isNotEmpty() && passwordConfirm.isNotEmpty() && phoneNumber.isNotEmpty() && authCode.isNotEmpty() + }.distinctUntilChanged() + .onEach { + updateButton(it) + }.launchIn(viewModelScope) + } + + private fun updateButton(enabled: Boolean) = intent { + reduce { + state.copy(isButtonEnabled = enabled) + } + } fun onIdChanged(id: String) = intent { reduce { @@ -27,55 +85,71 @@ class AccountSetupViewModel @Inject constructor( fun onPasswordChanged(password: String) = intent { reduce { - state.copy(password = password) + state.copy(password = password, isPasswordError = !password.isValidPassword()) } } fun onPasswordConfirmChanged(passwordConfirm: String) = intent { reduce { - state.copy(passwordConfirm = passwordConfirm) + state.copy( + passwordConfirm = passwordConfirm, + isPasswordConfirmError = state.password != passwordConfirm + ) } } - fun onEmailChanged(email: String) = intent { + fun onPhoneNumChanged(phoneNumber: String) = intent { reduce { - state.copy(email = email) + state.copy(phoneNumber = phoneNumber, isPhoneNumberError = phoneNumber.length != 11) } } - fun onNextButtonClicked() = intent { - postSideEffect(AccountAuthSideEffect.NavigateToNextScreen(state.email)) + fun onAuthCodeChanged(authCode: String) = intent { + reduce { + state.copy(authCode = authCode, signUpContinuationError = null) + } } fun onBackButtonClicked() = intent { - postSideEffect(AccountAuthSideEffect.NavigateToBackScreen) + postSideEffect(AccountSetupSideEffect.NavigateToBackScreen) } - fun checkInfo(email: String, password: String, passwordConfirm: String) { - intent { reduce { state.copy(isLoading = true) } } - viewModelScope.launch(Dispatchers.IO) { - sendSignupEmailUseCase(email, password, passwordConfirm) - .onSuccess { - intent { - reduce { state.copy(signupContinuationState = it)} - reduce { state.copy(signUpContinuationError = null)} + fun verifySmsCode( + password: String, passwordConfirm: String, phoneNumber: String, verifyCode: String + ) { + viewModelScope.launch { + businessSignupCheckUseCase( + password, passwordConfirm, phoneNumber, verifyCode + ).onSuccess { + intent { + reduce { + state.copy( + signupContinuationState = it, + signUpContinuationError = null + ) } + postSideEffect(AccountSetupSideEffect.NavigateToNextScreen(state.phoneNumber)) } - .onFailure { - intent { reduce { state.copy(signUpContinuationError = it) } } + }.onFailure { + intent { + reduce { state.copy(signUpContinuationError = it) } } - intent { reduce { state.copy(isLoading = false) } } + } } } - fun postEmailVerification(email: String) { - intent { reduce { state.copy(isLoading = true) } } + + fun sendSmsVerificationCode(phoneNumber: String) { viewModelScope.launch(Dispatchers.IO) { - sendSignupEmailUseCase.sendEmail(email) - .onSuccess {} - .onFailure { - intent { reduce { state.copy(signUpContinuationError = it) } } + sendSignupSmsCodeUseCase(phoneNumber).onSuccess { + intent { + reduce { state.copy(signupContinuationState = it) } + reduce { state.copy(signUpContinuationError = null) } + } + }.onFailure { + intent { + reduce { state.copy(signUpContinuationError = it) } } - intent { reduce { state.copy(isLoading = false) } } + } } } } \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthScreen.kt b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthScreen.kt index e2580d89c..d9e0276b7 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthScreen.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthScreen.kt @@ -1,8 +1,14 @@ package `in`.koreatech.business.feature.signup.businessauth -import androidx.compose.foundation.BorderStroke +import android.graphics.BitmapFactory.decodeStream +import android.provider.OpenableColumns +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Canvas -import androidx.compose.foundation.border +import androidx.compose.foundation.Image +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -16,239 +22,443 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text 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.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight.Companion.Bold import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.times +import androidx.hilt.navigation.compose.hiltViewModel import `in`.koreatech.business.R +import `in`.koreatech.business.feature.signup.accountsetup.AccountSetupViewModel import `in`.koreatech.business.feature.signup.dialog.BusinessAlertDialog import `in`.koreatech.business.feature.textfield.LinedTextField -import `in`.koreatech.business.ui.theme.ColorPrimary import `in`.koreatech.business.ui.theme.ColorDescription import `in`.koreatech.business.ui.theme.ColorDisabledButton -import `in`.koreatech.business.ui.theme.ColorSecondary -import `in`.koreatech.business.ui.theme.ColorHelper import `in`.koreatech.business.ui.theme.ColorMinor +import `in`.koreatech.business.ui.theme.ColorPrimary +import `in`.koreatech.business.ui.theme.ColorTextField +import `in`.koreatech.business.ui.theme.ColorUnarchived +import `in`.koreatech.business.ui.theme.Gray1 +import `in`.koreatech.business.ui.theme.Gray3 +import `in`.koreatech.koin.domain.model.store.AttachStore +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun BusinessAuthScreen( modifier: Modifier = Modifier, + accountSetupViewModel: AccountSetupViewModel = hiltViewModel(), + businessAuthViewModel: BusinessAuthViewModel = hiltViewModel(), + scrollState: ScrollState = rememberScrollState(), onBackClicked: () -> Unit = {}, onSearchClicked: () -> Unit = {}, onNextClicked: () -> Unit = {}, ) { - var name by remember { mutableStateOf("") } - var storeName by remember { mutableStateOf("") } - var storeNumber by remember { mutableStateOf("") } - var phoneNumber by remember { mutableStateOf("") } - val openAlertDialog = remember { mutableStateOf(false) } - val itemList: MutableList = mutableListOf() + val context = LocalContext.current + val scrollState = rememberScrollState() + val businessAuthState = businessAuthViewModel.collectAsState().value + val accountSetupState = accountSetupViewModel.collectAsState().value + + + val multiplePhotoPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickMultipleVisualMedia(5), + onResult = { uriList -> + var fileName = "" + var fileSize = 0L + businessAuthState.bitmap.clear() + businessAuthState.fileInfo.clear() + uriList.forEach { + val inputStream = context.contentResolver.openInputStream(it) + businessAuthViewModel.onImageUrlsChanged(mutableListOf()) + if (it.scheme.equals("content")) { + val cursor = context.contentResolver.query(it, null, null, null, null) + cursor.use { + if (cursor != null && cursor.moveToFirst()) { + val fileNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + val fileSizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + + if (fileNameIndex != -1 && fileSizeIndex != -1) { + fileName = cursor.getString(fileNameIndex) + fileSize = cursor.getLong(fileSizeIndex) + } + } + } + } + businessAuthViewModel.onImageUrlsChanged(uriList.map { + AttachStore( + it.toString(), + fileName + ) + }.toMutableList()) + if (inputStream != null) { + businessAuthViewModel.getPreSignedUrl( + uri = it, + fileName = fileName, + fileSize = fileSize, + fileType = "image/" + fileName.split(".")[1], + bitmap = decodeStream(inputStream) + ) + + } + inputStream?.close() + } + } + ) Column( - modifier = modifier, + modifier = modifier.fillMaxSize(), ) { - IconButton(modifier = Modifier.padding(vertical = 24.dp), onClick = { onBackClicked() }) { - Icon( - modifier = Modifier.padding(start = 10.dp), - painter = painterResource(id = R.drawable.ic_arrow_back), - contentDescription = stringResource(id = R.string.back_icon), - ) - } - Column( - modifier = Modifier - .padding(horizontal = 32.dp), - verticalArrangement = Arrangement.Center, - ) { - Text( - text = stringResource(id = R.string.master_sign_up), - fontSize = 24.sp, - fontWeight = Bold, - ) - Spacer(modifier = Modifier.height(40.dp)) - Row( + Column { + Box( modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + .fillMaxWidth() + .padding(vertical = 12.dp) ) { + IconButton( + onClick = { businessAuthViewModel.onNavigateToBackScreen() }, + modifier = Modifier.align(Alignment.CenterStart) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back), + contentDescription = stringResource(id = R.string.back_icon), + ) + } + Text( - color = ColorSecondary, text = stringResource(id = R.string.business_auth) - ) - Text( - color = ColorSecondary, text = stringResource(id = R.string.three_third) + text = stringResource(id = R.string.sign_up), + fontSize = 18.sp, + fontWeight = Bold, + modifier = Modifier.align(Alignment.Center) ) } - Canvas( + + Spacer(modifier = Modifier.height(20.dp)) + + Column( modifier = Modifier - .fillMaxWidth() - .padding(16.dp) + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center, ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier, + color = ColorPrimary, + fontWeight = Bold, + text = stringResource(id = R.string.business_auth) + ) + Text( + text = stringResource(id = R.string.three_third), + color = ColorPrimary, + fontWeight = Bold, + ) + } - drawLine( - color = ColorSecondary, - start = Offset(0f - 35, 0f), - end = Offset(size.width + 35, size.height), - strokeWidth = 4.dp.toPx(), - cap = StrokeCap.Round - ) + Canvas( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + drawLine( + color = ColorUnarchived, + start = Offset(-40f, 0f), + end = Offset(size.width + 35, size.height), + strokeWidth = 4.dp.toPx(), + cap = StrokeCap.Round + ) + drawLine( + color = ColorPrimary, + start = Offset(-40f, 0f), + end = Offset(size.width + 40, size.height), + strokeWidth = 4.dp.toPx(), + cap = StrokeCap.Round + ) + } } + } + + Spacer(modifier = Modifier.height(20.dp)) + Column( + modifier = Modifier + .padding(horizontal = 24.dp) + .verticalScroll(scrollState), + verticalArrangement = Arrangement.Center, + ) { Spacer(modifier = Modifier.height(10.dp)) - LinedTextField( - value = name, - onValueChange = { name = it }, - label = stringResource(id = R.string.master_name) - ) + Column { - Row( - modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween - ) { + + Text( + text = stringResource(id = R.string.master_name), + fontSize = 14.sp, + fontWeight = Bold + ) LinedTextField( - modifier = Modifier.width(197.dp), - value = storeName, - onValueChange = { storeName = it }, - label = stringResource(id = R.string.enter_store_name) + value = businessAuthState.name, + onValueChange = { businessAuthViewModel.onNameChanged(it) }, + label = stringResource(id = R.string.enter_name) ) - Button( - modifier = Modifier, - onClick = { onSearchClicked() }, - shape = RectangleShape, - colors = ButtonDefaults.buttonColors( - backgroundColor = ColorPrimary, - contentColor = Color.White, - ), + + Text( + text = stringResource(id = R.string.shop_name), + fontSize = 14.sp, + fontWeight = Bold + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, ) { - Text(text = stringResource(id = R.string.search_store)) + + LinedTextField( + modifier = Modifier.width(197.dp), + value = businessAuthState.shopName, + onValueChange = { + businessAuthViewModel.onShopNameChanged(it) + businessAuthViewModel.onShopIdChanged(null) + }, + label = stringResource(id = R.string.enter_store_name) + ) + + Button(modifier = Modifier + .height(41.dp), + shape = RoundedCornerShape(4.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = ColorPrimary, + contentColor = Color.White, + ), + onClick = { + businessAuthViewModel.onNavigateToSearchStore() + }) { + Text(text = stringResource(id = R.string.search_store)) + } } - } - LinedTextField( - value = storeNumber, - onValueChange = { storeNumber = it }, - label = stringResource(id = R.string.business_registration_number) - ) - LinedTextField( - value = phoneNumber, - onValueChange = { phoneNumber = it }, - label = stringResource(id = R.string.personal_contact) - ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(id = R.string.business_registration_number), + fontSize = 14.sp, + fontWeight = Bold + ) + LinedTextField( + value = businessAuthState.shopNumber, + onValueChange = { businessAuthViewModel.onStoreNumberChanged(it) }, + label = stringResource(id = R.string.enter_business_registration_number), + isError = businessAuthState.shopNumber.length != 10 && businessAuthState.shopNumber.isNotEmpty(), + errorText = stringResource(id = R.string.business_number_not_validate) + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(id = R.string.personal_contact), + fontSize = 14.sp, + fontWeight = Bold + ) + LinedTextField( + value = businessAuthState.phoneNumber, + onValueChange = { businessAuthViewModel.onPhoneNumberChanged(it) }, + label = stringResource(id = R.string.enter_personal_contact), + isError = businessAuthState.phoneNumber.length != 11 && businessAuthState.phoneNumber.isNotEmpty(), + errorText = stringResource(id = R.string.phone_number_not_validate) + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(id = R.string.instruction_file), + fontSize = 14.sp, + fontWeight = Bold + ) + Text( + text = stringResource(id = R.string.file_upload_instruction), + fontSize = 12.sp, + color = ColorDescription + ) + Column( + modifier = Modifier + .fillMaxWidth(), + ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(125.dp), - ) { + if (businessAuthState.selectedImages.isNotEmpty()) { + UploadFileList( + modifier, + businessAuthState.selectedImages, + ) { + val list = mutableListOf() + businessAuthState.selectedImages.forEach { + list.add(it.title) + } + list.removeAt(it) + businessAuthViewModel.onImageUrlsChanged( + list.map { + AttachStore( + it, + it + ) + }.toMutableList() + ) + } + Spacer(modifier = Modifier.height(10.dp)) + } - if (itemList.isNotEmpty()) UploadFileList(modifier, itemList) - else - Column( + Button( modifier = Modifier - .fillMaxSize() - .height(125.dp) - .border(BorderStroke(1.dp, ColorHelper)) - .clickable { }, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { + .fillMaxWidth() + .height(44.dp), + shape = RectangleShape, + enabled = businessAuthState.selectedImages.isEmpty(), + colors = ButtonDefaults.buttonColors( + backgroundColor = ColorTextField, + contentColor = Gray1, + disabledBackgroundColor = ColorTextField, + disabledContentColor = Gray3, + ), + onClick = { businessAuthViewModel.onDialogVisibilityChanged(true) }) { Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(id = R.drawable.plus_square), - contentDescription = stringResource(id = R.string.upload_file_icon), - tint = ColorSecondary + painter = painterResource(id = R.drawable.attach_file_add), + contentDescription = stringResource(id = R.string.attach_file) ) Text( - text = stringResource(id = R.string.file_upload_prompt), - fontSize = 12.sp, + text = stringResource(id = R.string.file_upload), + fontSize = 13.sp, fontWeight = Bold, - color = Color.Black - ) - Text( - text = stringResource(id = R.string.file_upload_instruction), - fontSize = 11.sp, - color = ColorDescription, ) } + } - } + Spacer(modifier = Modifier.height(60.dp)) + Button(modifier = Modifier + .fillMaxWidth() + .height(44.dp), + shape = RoundedCornerShape(4.dp), + enabled = businessAuthState.isButtonEnabled, + colors = ButtonDefaults.buttonColors( + backgroundColor = ColorPrimary, + disabledBackgroundColor = ColorDisabledButton, + contentColor = Color.White, + disabledContentColor = Color.White, + ), - Spacer(modifier = Modifier.height(47.dp)) - Button(modifier = Modifier - .fillMaxWidth() - .height(44.dp), - shape = RectangleShape, - colors = ButtonDefaults.buttonColors( - backgroundColor = ColorPrimary, - disabledBackgroundColor = ColorDisabledButton, - contentColor = Color.White, - disabledContentColor = Color.White, - ), - onClick = { onNextClicked() }) { - Text( - text = stringResource(id = R.string.next), - fontSize = 15.sp, - color = Color.White, - ) + onClick = { + businessAuthState.fileInfo.forEach { + businessAuthViewModel.uploadImage( + it.preSignedUrl, + businessAuthState.bitmap[businessAuthState.fileInfo.indexOf(it)].toString(), + it.mediaType, + it.fileSize, + ) + } + businessAuthViewModel.sendRegisterRequest( + businessAuthState.fileInfo.map { it.resultUrl }, + businessAuthState.shopNumber, + accountSetupState.phoneNumber, + businessAuthState.name, + accountSetupState.password, + businessAuthState.phoneNumber, + businessAuthState.shopId, + businessAuthState.shopName, + ) - BusinessAlertDialog( - onDismissRequest = { openAlertDialog.value = false }, - onConfirmation = { - openAlertDialog.value = false - }, - dialogTitle = stringResource(id = R.string.file_upload), - dialogText = stringResource(id = R.string.file_upload_requirements), - positiveButtonText = stringResource(id = R.string.select_file) - ) + }) { + Text( + text = stringResource(id = R.string.next), + fontSize = 15.sp, + color = Color.White, + ) + BusinessAlertDialog( + onDismissRequest = { businessAuthViewModel.onDialogVisibilityChanged(false) }, + onConfirmation = { + multiplePhotoPickerLauncher.launch( + PickVisualMediaRequest( + ActivityResultContracts.PickVisualMedia.ImageOnly + ) + ) + businessAuthViewModel.onDialogVisibilityChanged(false) + }, + dialogTitle = stringResource(id = R.string.file_upload), + dialogText = stringResource(id = R.string.file_upload_requirements), + positiveButtonText = stringResource(id = R.string.select_file), + visibility = businessAuthState.dialogVisibility + ) + } } + } + businessAuthViewModel.collectSideEffect { + when (it) { + BusinessAuthSideEffect.NavigateToSearchStore -> { + onSearchClicked() + } + + BusinessAuthSideEffect.NavigateToBackScreen -> { + onBackClicked() + } + BusinessAuthSideEffect.NavigateToNextScreen -> { + onNextClicked() + } + } } } } @Composable -fun UploadFileList(modifier: Modifier, item: MutableList) { - LazyColumn( - modifier = modifier - .fillMaxSize() - .border(BorderStroke(1.dp, ColorHelper)), - ) { +fun UploadFileList( + modifier: Modifier, + fileList: MutableList, + onDelete: (Int) -> Unit = {} +) { + Column(modifier = Modifier.height(fileList.size * 40.dp)) { - items(item.size) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( + LazyColumn( + modifier = modifier + .fillMaxSize(), + ) { + items(fileList.size) { + Row( modifier = Modifier - .size(24.dp) - .padding(end = 8.dp), - painter = painterResource(id = R.drawable.file_icon), - contentDescription = stringResource(id = R.string.file_icon), - tint = ColorMinor, - ) - Text(text = "", fontSize = 15.sp, color = ColorMinor) + .fillMaxWidth() + .background(ColorTextField) + .padding(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + + Image( + modifier = Modifier + .size(24.dp) + .clickable { + onDelete(it) + } + .padding(end = 8.dp), + painter = painterResource(id = R.drawable.ic_delete_button), + contentDescription = stringResource(id = R.string.file_icon), + ) + + Text(text = fileList[it].title, fontSize = 15.sp, color = ColorMinor) + } + Spacer(modifier = Modifier.width(12.dp)) } - Spacer(modifier = Modifier.width(12.dp)) } } } + diff --git a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthSideEffect.kt index 5a6dd99db..140864280 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthSideEffect.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthSideEffect.kt @@ -1,5 +1,9 @@ package `in`.koreatech.business.feature.signup.businessauth +import `in`.koreatech.business.feature.signup.accountsetup.AccountSetupSideEffect + sealed class BusinessAuthSideEffect { - object ShowDialog : BusinessAuthSideEffect() + data object NavigateToSearchStore : BusinessAuthSideEffect() + data object NavigateToBackScreen : BusinessAuthSideEffect() + data object NavigateToNextScreen : BusinessAuthSideEffect() } \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthState.kt b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthState.kt index 55e87565f..e454ddfbb 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthState.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthState.kt @@ -1,5 +1,23 @@ package `in`.koreatech.business.feature.signup.businessauth +import android.graphics.Bitmap +import `in`.koreatech.koin.domain.model.store.AttachStore +import `in`.koreatech.koin.domain.model.store.StoreUrl + data class BusinessAuthState( - val count: Int = 0 -) + val name: String = "", + val shopName: String = "", + val shopNumber: String = "", + val shopId: Int? = null, + val phoneNumber: String = "", + val openAlertDialog: Boolean = false, + val selectedImages :MutableList = mutableListOf(), + val dialogVisibility:Boolean = false, + val fileInfo: MutableList = mutableListOf(), + val bitmap: MutableList = mutableListOf(), + val continuation: Boolean = false, + val error: Throwable? = null, +){ + val isButtonEnabled: Boolean + get() = name.isNotEmpty() && shopName.isNotEmpty() && shopNumber.isNotEmpty() && phoneNumber.isNotEmpty() && selectedImages.isNotEmpty() +} diff --git a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthViewModel.kt b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthViewModel.kt index b58e0e2e2..4b5638605 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthViewModel.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/BusinessAuthViewModel.kt @@ -1,19 +1,180 @@ package `in`.koreatech.business.feature.signup.businessauth +import android.graphics.Bitmap +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import `in`.koreatech.koin.core.viewmodel.BaseViewModel +import `in`.koreatech.koin.data.mapper.strToOwnerRegisterUrl +import `in`.koreatech.koin.domain.model.store.AttachStore +import `in`.koreatech.koin.domain.model.store.StoreUrl +import `in`.koreatech.koin.domain.usecase.business.UploadFileUseCase +import `in`.koreatech.koin.domain.usecase.owner.AttachStoreFileUseCase +import `in`.koreatech.koin.domain.usecase.owner.OwnerRegisterUseCase +import `in`.koreatech.koin.domain.util.ext.formatBusinessNumber +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class BusinessAuthViewModel @Inject constructor( + private val getPresignedUrlUseCase: AttachStoreFileUseCase, + private val uploadFilesUseCase: UploadFileUseCase, + private val ownerRegisterUseCase: OwnerRegisterUseCase +) : ContainerHost, ViewModel() { + override val container = + container(BusinessAuthState()) -) : ContainerHost, BaseViewModel() { - override val container = container(BusinessAuthState()) - fun onShowDialog() = intent { - postSideEffect(BusinessAuthSideEffect.ShowDialog) + fun onNameChanged(name: String) = intent { + reduce { + state.copy(name = name) + } + } + + fun onShopNameChanged(storeName: String) = intent { + reduce { + state.copy(shopName = storeName) + } + } + + fun onShopIdChanged(shopId: Int?) = intent { + reduce { + state.copy(shopId = shopId) + } + } + + fun onStoreNumberChanged(storeNumber: String) = intent { + reduce { + state.copy(shopNumber = storeNumber) + } + } + + fun onPhoneNumberChanged(phoneNumber: String) = intent { + reduce { + state.copy(phoneNumber = phoneNumber) + } + } + + fun onImageUrlsChanged(selectedImages: MutableList) = intent { + reduce { + state.copy(selectedImages = selectedImages) + } + } + + fun onDialogVisibilityChanged(dialogVisibility: Boolean) = intent { + reduce { + state.copy(dialogVisibility = dialogVisibility) + } + } + + fun onNavigateToSearchStore() = intent { + postSideEffect(BusinessAuthSideEffect.NavigateToSearchStore) + } + + fun onNavigateToBackScreen() = intent { + postSideEffect(BusinessAuthSideEffect.NavigateToBackScreen) + } + + fun onNavigateToNextScreen() = intent { + postSideEffect(BusinessAuthSideEffect.NavigateToNextScreen) + } + + fun getPreSignedUrl( + uri: Uri, + fileSize: Long, + fileType: String, + fileName: String, + bitmap: Bitmap + ) { + viewModelScope.launch { + getPresignedUrlUseCase( + fileSize, fileType, fileName + ).onSuccess { + intent { + reduce { + state.copy( + fileInfo = state.fileInfo.toMutableList().apply { + add( + StoreUrl( + uri.toString(), + it.first, + fileName, + fileType, + it.second, + fileSize + ) + ) + }, + bitmap = state.bitmap.toMutableList().apply { + add(bitmap) + }, + error = null + ) + } + } + }.onFailure { + intent { + reduce { state.copy(error = it) } + } + } + } + } + + fun uploadImage( + url: String, + bitmap: String, + mediaType: String, + mediaSize: Long + ) { + viewModelScope.launch(Dispatchers.IO) { + uploadFilesUseCase(url, bitmap, mediaType, mediaSize).onSuccess { + intent { + reduce { state.copy(error = null) } + } + }.onFailure { + intent { + reduce { state.copy(error = it) } + } + } + } + } + + fun sendRegisterRequest( + fileUrls: List, + companyNumber: String, + email: String, + name: String, + password: String, + phoneNumber: String, + shopId: Int?, + shopName: String + ) { + viewModelScope.launch { + ownerRegisterUseCase( + fileUrls.strToOwnerRegisterUrl(), + companyNumber.formatBusinessNumber(), + email, + name, + password, + phoneNumber, + shopId, + shopName + ).onSuccess { + onNavigateToNextScreen() + intent { + reduce { state.copy(continuation = true) } + } + }.onFailure { + intent { + reduce { state.copy(error = it) } + } + } + + } } } \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreScreen.kt b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreScreen.kt index 9460c8e25..0e95a5851 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreScreen.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreScreen.kt @@ -1,10 +1,11 @@ package `in`.koreatech.business.feature.signup.businessauth import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background +import androidx.compose.foundation.Canvas import androidx.compose.foundation.border import androidx.compose.foundation.clickable 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 @@ -15,27 +16,24 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.IconButton -import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.material.TextFieldDefaults import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -43,133 +41,178 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight.Companion.Bold import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import `in`.koreatech.business.R -import `in`.koreatech.business.ui.theme.ColorActiveButton +import `in`.koreatech.business.feature.textfield.SearchTextField import `in`.koreatech.business.ui.theme.ColorDescription import `in`.koreatech.business.ui.theme.ColorHelper import `in`.koreatech.business.ui.theme.ColorPrimary -import `in`.koreatech.business.ui.theme.ColorSearch -import `in`.koreatech.business.ui.theme.ColorSecondary -import `in`.koreatech.business.ui.theme.KOIN_ANDROIDTheme +import `in`.koreatech.business.ui.theme.ColorTextField +import `in`.koreatech.business.ui.theme.Gray1 +import `in`.koreatech.business.ui.theme.Gray2 +import `in`.koreatech.koin.domain.model.store.Store import kotlinx.coroutines.launch -import okhttp3.internal.immutableListOf +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @Composable -fun SearchStoreScreen(modifier: Modifier = Modifier, onBackClicked: () -> Unit = {}) { - var search by remember { mutableStateOf("") } - val storeItems = immutableListOf("") - +fun SearchStoreScreen( + modifier: Modifier = Modifier, + viewModel: SearchStoreViewModel = hiltViewModel(), + businessAuthViewModel: BusinessAuthViewModel = hiltViewModel(), + onBackButtonClicked: () -> Unit = {} +) { + val state = viewModel.collectAsState().value Column( modifier = modifier, + ) { - IconButton( - modifier = Modifier.padding(vertical = 24.dp), - onClick = { onBackClicked() } - ) { - Icon( - modifier = Modifier.padding(start = 10.dp), - painter = painterResource(id = R.drawable.ic_arrow_back), - contentDescription = stringResource(id = R.string.back_icon), - ) - } - Column( + Box( modifier = Modifier - .padding(horizontal = 32.dp) - .background(Color.White), - verticalArrangement = Arrangement.Center, + .fillMaxWidth() + .padding(vertical = 12.dp) ) { + IconButton( + onClick = { viewModel.onNavigateToBackScreen() }, + modifier = Modifier.align(Alignment.CenterStart) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back), + contentDescription = stringResource(id = R.string.back_icon), + ) + } - Text( - text = stringResource(id = R.string.master_sign_up), - fontSize = 24.sp, - fontWeight = Bold, - ) - Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(id = R.string.search_store), fontSize = 18.sp, fontWeight = Bold, + modifier = Modifier.align(Alignment.Center) ) } + + Column( + modifier = Modifier + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center, + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier, + color = ColorPrimary, + fontWeight = Bold, + text = stringResource(id = R.string.business_auth), + ) + Text( + text = stringResource(id = R.string.three_third), + color = ColorPrimary, + fontWeight = Bold, + ) + } + + Canvas( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + + drawLine( + color = ColorPrimary, + start = Offset(-40f, 0f), + end = Offset((size.width + 40), size.height), + strokeWidth = 4.dp.toPx(), + cap = StrokeCap.Round + ) + } + } Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) ) { - Spacer(modifier = Modifier.height(42.dp)) - TextField( + Spacer(modifier = Modifier.height(10.dp)) + SearchTextField( modifier = Modifier .fillMaxWidth() - .height(50.dp), - value = search, onValueChange = { search = it }, + .height(45.dp), + value = state.search, + onValueChange = { viewModel.onSearchChanged(it) + viewModel.onSearchStore()}, + label = stringResource(id = R.string.search_shop), textStyle = TextStyle(fontSize = 14.sp), + ) + Spacer(modifier = Modifier.height(5.dp)) + StoreList(state.stores, viewModel, businessAuthViewModel) - colors = TextFieldDefaults.textFieldColors( - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedLabelColor = ColorSearch, - ), - placeholder = { - Text( - text = stringResource(id = R.string.search_shop), - color = ColorDescription, - fontSize = 13.sp - ) - }, - trailingIcon = { - IconButton(onClick = { }) { - Icon( - modifier = Modifier.size(17.dp), - painter = painterResource(id = R.drawable.search), - contentDescription = stringResource(id = R.string.search_icon) - ) - } + } + + + viewModel.collectSideEffect { + when (it) { + is SearchStoreSideEffect.SearchStore -> { + viewModel.searchStore() } - ) - Spacer(modifier = Modifier.height(16.dp)) - StoreList(storeItems, onSelected = {}) + + SearchStoreSideEffect.NavigateToBackScreen -> { + onBackButtonClicked() + } + } } } + } @OptIn(ExperimentalMaterialApi::class) @Composable -fun StoreList(item: List, onSelected: () -> Unit = {}) { - var selectedItemIndex by remember { mutableStateOf(-1) } +fun StoreList( + item: List, + viewModel: SearchStoreViewModel = hiltViewModel(), + businessAuthViewModel: BusinessAuthViewModel = hiltViewModel() +) { + val state = viewModel.collectAsState().value + val selectedStoreState = businessAuthViewModel.collectAsState().value val sheetState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden ) val scope = rememberCoroutineScope() - Scaffold() { contentPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .height(200.dp) + + ) { LazyColumn( modifier = Modifier + .weight(1f) + .padding(vertical = 8.dp) .fillMaxSize() - .padding(contentPadding), - horizontalAlignment = Alignment.CenterHorizontally, ) { items(item.size) { index -> Row( modifier = Modifier .padding(vertical = 8.dp) .fillMaxWidth() - .height(59.dp) + .height(55.dp) .clickable { - selectedItemIndex = index scope.launch { sheetState.show() } + viewModel.onItemIndexChange(index) } .border( BorderStroke( - width = if (selectedItemIndex == index) 1.5.dp else 1.dp, - color = if (selectedItemIndex == index) ColorPrimary else ColorHelper - ) + width = if (state.itemIndex == index) 1.5.dp else 1.dp, + color = if (state.itemIndex == index) ColorPrimary else ColorHelper + ), + shape = RoundedCornerShape(6.dp), ), ) { Row( @@ -181,103 +224,55 @@ fun StoreList(item: List, onSelected: () -> Unit = {}) { ) { Text( modifier = Modifier, - text = stringResource(id = R.string.store_name), + text = item[index].name, fontSize = 15.sp, color = Color.Black, - fontWeight = Bold - ) + + ) Spacer(modifier = Modifier.width(74.dp)) Text( buildAnnotatedString { - withStyle(style = SpanStyle(color = ColorSecondary)) { + withStyle(style = SpanStyle(color = ColorPrimary)) { append(text = stringResource(id = R.string.delivery)) } append(" ") - withStyle(style = SpanStyle(color = ColorSecondary)) { + withStyle(style = SpanStyle(color = ColorPrimary)) { append(text = stringResource(id = R.string.card_payment)) } append(" ") - withStyle(style = SpanStyle(color = ColorSecondary)) { + withStyle(style = SpanStyle(color = ColorPrimary)) { append(text = stringResource(id = R.string.account_transfer)) } }, - fontSize = 15.sp + fontSize = 12.sp ) } } } } + Spacer(modifier =Modifier.height(70.dp)) - ModalBottomSheetLayout( - modifier = Modifier, - sheetState = sheetState, - sheetElevation = 8.dp, - sheetContent = { - StoreBottomSheet { - onSelected() - } - }) { - } - - } -} - -@Composable -fun StoreBottomSheet(onSelected: () -> Unit) { - Row( - modifier = Modifier.background(Color.White) - .fillMaxWidth().height(118.dp) - .padding(20.dp), - - ) { - Column(modifier = Modifier,) { + Button(modifier = Modifier + .fillMaxWidth() + .height(44.dp), + shape = RoundedCornerShape(4.dp), + enabled = state.itemIndex > -1, + colors = ButtonDefaults.buttonColors( + backgroundColor = ColorPrimary, + contentColor = Color.White, + disabledBackgroundColor = Gray2, + disabledContentColor = Gray1, + ), + onClick = { + viewModel.onNavigateToBackScreen() + selectedStoreState.shopId?.let { businessAuthViewModel.onShopIdChanged(state.itemIndex) } + businessAuthViewModel.onShopNameChanged(state.stores[state.itemIndex].name) + }) { Text( - modifier = Modifier.padding(bottom = 8.dp), - text = stringResource(id = R.string.store_name), - fontSize = 18.sp, - color = Color.Black, + text = stringResource(id = R.string.next), + fontSize = 13.sp, fontWeight = Bold, ) - Row( modifier=Modifier, - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(id = R.string.phone_number), - fontSize = 14.sp, - color = ColorActiveButton, - fontWeight = Bold - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "", fontSize = 14.sp, color = Color.Black, - fontWeight = Bold - ) - - } - - } - Button( - modifier = Modifier - .fillMaxWidth() - .padding(start = 100.dp) - .height(55.dp) - , - onClick = { onSelected() }, - shape = RectangleShape, - ) { - Text(text = stringResource(id = R.string.select), fontWeight = Bold) } } - } - - -@Preview -@Composable -fun PreviewSearchStoreScreen() { - KOIN_ANDROIDTheme { - // StoreList(ImmutableList.of("")) - StoreBottomSheet(onSelected = {}) - } - -} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreSideEffect.kt new file mode 100644 index 000000000..0ebe93d6c --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreSideEffect.kt @@ -0,0 +1,7 @@ +package `in`.koreatech.business.feature.signup.businessauth + + +sealed class SearchStoreSideEffect { + data class SearchStore(val search: String) : SearchStoreSideEffect() + data object NavigateToBackScreen : SearchStoreSideEffect() +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreState.kt b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreState.kt new file mode 100644 index 000000000..0cdd89e6d --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreState.kt @@ -0,0 +1,11 @@ +package `in`.koreatech.business.feature.signup.businessauth + +import `in`.koreatech.koin.domain.model.store.Store +import kotlinx.coroutines.Job + +data class SearchStoreState( + val search: String= "", + val stores: List = emptyList(), + val itemIndex: Int = -1, + val searchJob: Job? = null +) \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreViewModel.kt b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreViewModel.kt new file mode 100644 index 000000000..1cd0c236e --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/businessauth/SearchStoreViewModel.kt @@ -0,0 +1,63 @@ +package `in`.koreatech.business.feature.signup.businessauth + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import `in`.koreatech.koin.domain.usecase.business.SearchStoresUseCase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class SearchStoreViewModel @Inject constructor( + private val searchStoresUseCase: SearchStoresUseCase, +) : ContainerHost, ViewModel() { + override val container = + container(SearchStoreState()) + + init { + searchStore() + } + + fun onItemIndexChange(index: Int) = intent { + reduce { + state.copy(itemIndex = index) + } + } + + fun onSearchChanged(search: String) = intent { + reduce { + state.copy(search = search) + } + } + + + fun searchStore() = intent { + viewModelScope.launch { + val newSearchJob = launch { + searchStoresUseCase(state.search).let { stores -> + reduce { + state.copy(stores = stores) + } + } + } + reduce { + state.copy(searchJob = newSearchJob) + } + } + } + + fun onSearchStore() = intent { + postSideEffect(SearchStoreSideEffect.SearchStore(state.search)) + } + + fun onNavigateToBackScreen() = intent { + postSideEffect(SearchStoreSideEffect.NavigateToBackScreen) + } + +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermScreen.kt b/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermScreen.kt new file mode 100644 index 000000000..8fcd74aa9 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermScreen.kt @@ -0,0 +1,298 @@ +package `in`.koreatech.business.feature.signup.checkterm + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import `in`.koreatech.business.R +import `in`.koreatech.business.ui.theme.Blue1 +import `in`.koreatech.business.ui.theme.ColorPrimary +import `in`.koreatech.business.ui.theme.ColorTextField +import `in`.koreatech.business.ui.theme.ColorUnarchived +import `in`.koreatech.business.ui.theme.Gray5 +import `in`.koreatech.business.ui.theme.Gray6 +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect + + +@Composable +fun CheckTermScreen( + modifier: Modifier = Modifier, + viewModel: CheckTermViewModel = hiltViewModel(), + onBackClicked: () -> Unit = {}, + onNextClicked: () -> Unit = {} +) { + val state = viewModel.collectAsState().value + val scrollState = rememberScrollState() + val scrollStatePrivacy = rememberScrollState() + val scrollStateKoin = rememberScrollState() + + Column( + modifier = modifier.fillMaxSize(), + ) { + Column { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + ) { + IconButton( + onClick = { viewModel.onBackButtonClicked() }, + modifier = Modifier.align(Alignment.CenterStart) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back), + contentDescription = stringResource(id = R.string.back_icon), + ) + } + + Text( + text = stringResource(id = R.string.sign_up), + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.Center) + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Column( + modifier = Modifier + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center, + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier, + color = ColorPrimary, + fontWeight = FontWeight.Bold, + text = stringResource(id = R.string.check_terms) + ) + Text( + text = stringResource(id = R.string.one_third), + color = ColorPrimary, + fontWeight = FontWeight.Bold, + ) + } + + Canvas( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + drawLine( + color = ColorUnarchived, + start = Offset(-40f, 0f), + end = Offset(size.width + 35, size.height), + strokeWidth = 4.dp.toPx(), + cap = StrokeCap.Round + ) + drawLine( + color = ColorPrimary, + start = Offset(-40f, 0f), + end = Offset((size.width + 40) / 3, size.height), + strokeWidth = 4.dp.toPx(), + cap = StrokeCap.Round + ) + } + } + } + + Column( + modifier = Modifier + .padding(horizontal = 24.dp).verticalScroll(scrollState), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = modifier + .fillMaxWidth() + .height(40.dp) + .background(color = ColorTextField, shape = RoundedCornerShape(4.dp)), + verticalAlignment = Alignment.CenterVertically + + ) { + Image( + painter = if (state.isAllTermChecked) painterResource(id = R.drawable.ic_check_selected) else painterResource( + id = R.drawable.ic_check + ), + contentDescription = stringResource(R.string.check), + modifier = modifier + .padding(horizontal = 8.dp) + .height(24.dp) + .width(24.dp) + .clickable { + viewModel.onAllTermCheckedChanged() + } + ) + + Text( + text = stringResource(R.string.check_all), + color = ColorPrimary, + fontWeight = FontWeight.Bold, + fontSize = 15.sp + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + modifier = modifier + .fillMaxWidth() + .height(22.dp) + + ) { + Image( + painter = if (state.isCheckedPrivacyTerms || state.isAllTermChecked) painterResource( + id = R.drawable.ic_check_selected + ) else painterResource( + id = R.drawable.ic_check + ), + contentDescription = stringResource(R.string.check), + modifier = modifier + .padding(horizontal = 8.dp) + .height(24.dp) + .width(24.dp) + .clickable { + viewModel.onPrivacyTermCheckedChanged() + + } + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(R.string.private_info_term), + color = Gray6, + fontWeight = FontWeight.Bold, + fontSize = 15.sp + ) + } + Spacer(modifier = Modifier.height(15.dp)) + Box( + modifier = Modifier + .padding(bottom = 16.dp) + .border(width = 1.dp, color = Blue1, shape = RectangleShape) + .padding(10.dp) + .height(143.dp) + .verticalScroll(scrollStatePrivacy) + ) { + Text(text = stringResource(R.string.term_1), fontSize = 10.sp, color = Color.Black) + } + + Spacer(modifier = Modifier.height(15.dp)) + + Row( + modifier = modifier + .fillMaxWidth() + .height(22.dp) + + ) { + Image( + painter = if (state.isCheckedKoinTerms || state.isAllTermChecked) painterResource( + id = R.drawable.ic_check_selected + ) else painterResource( + id = R.drawable.ic_check + ), + contentDescription = stringResource(R.string.check), + modifier = modifier + .padding(horizontal = 8.dp) + .height(24.dp) + .width(24.dp) + .clickable { + viewModel.onKoinTermCheckedChanged() + } + ) + + Text( + text = stringResource(R.string.koin_essential_term), + color = Gray6, + fontWeight = FontWeight.Bold, + fontSize = 15.sp + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + Box( + modifier = Modifier + .padding(bottom = 16.dp) + .border(width = 1.dp, color = Blue1, shape = RectangleShape) + .padding(10.dp) + .height(143.dp) + .verticalScroll(scrollStateKoin) + ) { + Text(text = stringResource(R.string.term_2), fontSize = 10.sp, color = Color.Black) + } + + Spacer(modifier = Modifier.height(50.dp)) + + Button( + modifier = modifier + .fillMaxWidth() + .height(44.dp), + onClick = { viewModel.onNextButtonClicked() }, + shape = RoundedCornerShape(4.dp), + colors = if (state.isAllTermChecked) ButtonDefaults.buttonColors(ColorPrimary) else ButtonDefaults.buttonColors( + Gray5 + ), + + ) { + Text(text = stringResource(R.string.next)) + } + + } + } + viewModel.collectSideEffect { + when (it) { + CheckTermSideEffect.NavigateToNextScreen -> onNextClicked() + CheckTermSideEffect.NavigateToBackScreen -> onBackClicked() + } + } +} + +@Preview +@Composable +fun previewTermPage() { + Surface( + modifier = Modifier.fillMaxSize(), + ) { + CheckTermScreen() + } +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermSideEffect.kt new file mode 100644 index 000000000..f98841ca7 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermSideEffect.kt @@ -0,0 +1,6 @@ +package `in`.koreatech.business.feature.signup.checkterm + +sealed class CheckTermSideEffect { + data object NavigateToNextScreen : CheckTermSideEffect() + data object NavigateToBackScreen : CheckTermSideEffect() +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermState.kt b/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermState.kt new file mode 100644 index 000000000..e69678393 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermState.kt @@ -0,0 +1,7 @@ +package `in`.koreatech.business.feature.signup.checkterm + +data class CheckTermState( + val isAllTermChecked: Boolean = false, + val isCheckedPrivacyTerms: Boolean = false, + val isCheckedKoinTerms: Boolean = false +) \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermViewModel.kt b/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermViewModel.kt new file mode 100644 index 000000000..f159bde99 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/checkterm/CheckTermViewModel.kt @@ -0,0 +1,70 @@ +package `in`.koreatech.business.feature.signup.checkterm + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + + +@HiltViewModel +class CheckTermViewModel @Inject constructor() : + ContainerHost, ViewModel() { + override val container = + container(CheckTermState()) + + fun onAllTermCheckedChanged() { + intent { + reduce { + state.copy( + isAllTermChecked = !state.isAllTermChecked, + isCheckedPrivacyTerms = !state.isAllTermChecked, + isCheckedKoinTerms = !state.isAllTermChecked + ) + } + } + } + + fun onPrivacyTermCheckedChanged() { + intent { + if (state.isAllTermChecked) { + reduce { state.copy(isAllTermChecked = false) } + } + reduce { state.copy(isCheckedPrivacyTerms = !state.isCheckedPrivacyTerms) } + checkAllTermChecked() + } + } + + fun onKoinTermCheckedChanged() { + intent { + if (state.isAllTermChecked) { + reduce { state.copy(isAllTermChecked = false) } + } + reduce { state.copy(isCheckedKoinTerms = !state.isCheckedKoinTerms) } + checkAllTermChecked() + } + } + + private fun checkAllTermChecked() { + intent { + if (state.isCheckedPrivacyTerms && state.isCheckedKoinTerms) { + reduce { state.copy(isAllTermChecked = true) } + } + } + } + + fun onNextButtonClicked() { + intent { + postSideEffect(CheckTermSideEffect.NavigateToNextScreen) + } + } + + fun onBackButtonClicked() { + intent { + postSideEffect(CheckTermSideEffect.NavigateToBackScreen) + } + } +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/completesignup/CompleteSignupScreen.kt b/business/src/main/java/in/koreatech/business/feature/signup/completesignup/CompleteSignupScreen.kt index 46d146ce2..a0fcf59a4 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/completesignup/CompleteSignupScreen.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/completesignup/CompleteSignupScreen.kt @@ -1,52 +1,125 @@ package `in`.koreatech.business.feature.signup.completesignup +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.Center +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.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight.Companion.Bold import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import `in`.koreatech.business.R import `in`.koreatech.business.ui.theme.ColorDescription import `in`.koreatech.business.ui.theme.ColorDisabledButton import `in`.koreatech.business.ui.theme.ColorPrimary -import `in`.koreatech.business.ui.theme.ColorSecondary +import `in`.koreatech.business.ui.theme.ColorUnarchived +import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun CompleteSignupScreen( modifier: Modifier = Modifier, + viewModel: CompleteSignupViewModel= hiltViewModel(), + onNavigateToLoginScreen: () -> Unit = {}, onBackClicked: () -> Unit = {} ) { Column( modifier = modifier, ) { - IconButton( - modifier = Modifier.padding(vertical = 24.dp), - onClick = { onBackClicked() } + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) ) { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_back), - contentDescription = stringResource(id = R.string.back_icon), + IconButton( + onClick = { viewModel.onBackButtonClicked() }, + modifier = Modifier.align(Alignment.CenterStart) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_back), + contentDescription = stringResource(id = R.string.back_icon), + ) + } + + Text( + text = stringResource(id = R.string.sign_up), + fontSize = 18.sp, + fontWeight = Bold, + modifier = Modifier.align(Alignment.Center) ) } + Column( + modifier = Modifier + .padding(horizontal = 32.dp), + verticalArrangement = Arrangement.Center, + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + modifier = Modifier, + color = ColorPrimary, + fontWeight = Bold, + text = stringResource(id = R.string.business_auth), + ) + Text( + text = stringResource(id = R.string.three_third), + color = ColorPrimary, + fontWeight = Bold, + ) + } + + Canvas( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + drawLine( + color = ColorUnarchived, + start = Offset(-40f, 0f), + end = Offset(size.width + 35, size.height), + strokeWidth = 4.dp.toPx(), + cap = StrokeCap.Round + ) + drawLine( + color = ColorPrimary, + start = Offset(-40f, 0f), + end = Offset((size.width + 40), size.height), + strokeWidth = 4.dp.toPx(), + cap = StrokeCap.Round + ) + } + + } Spacer(modifier = Modifier.height(123.dp)) Column( @@ -54,12 +127,12 @@ fun CompleteSignupScreen( verticalArrangement = Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Icon( - modifier = Modifier.size(55.dp), - painter = painterResource(id = R.drawable.signup_check), - contentDescription = stringResource(id = R.string.check_icon), - tint = ColorSecondary + Image( + modifier = Modifier.size(276.dp), + painter = painterResource(id = R.drawable.complete_signup), + contentDescription = stringResource(id = R.string.signup_request_complete), ) + Spacer(modifier = Modifier.height(25.dp)) Text( @@ -82,14 +155,14 @@ fun CompleteSignupScreen( Button(modifier = Modifier .fillMaxWidth() .height(44.dp), - shape = RectangleShape, + shape = RoundedCornerShape(4.dp), colors = ButtonDefaults.buttonColors( backgroundColor = ColorPrimary, disabledBackgroundColor = ColorDisabledButton, contentColor = Color.White, disabledContentColor = Color.White, ), - onClick = { onBackClicked() }) { + onClick = { viewModel.onNavigateToLoginScreen() }) { Text( text = stringResource(id = R.string.navigate_to_login_screen), fontSize = 15.sp, @@ -99,6 +172,33 @@ fun CompleteSignupScreen( } } + viewModel.collectSideEffect { + when (it) { + is CompleteSignupSideEffect.NavigateToLoginScreen -> { + onNavigateToLoginScreen() + } + is CompleteSignupSideEffect.NavigateToBackScreen -> { + onBackClicked() + } + } + } + + } +} + + +@Composable +@Preview +fun Preview() { + Surface( + modifier = Modifier + .fillMaxSize() + + ) { + + CompleteSignupScreen( + onBackClicked = { } + ) } } diff --git a/business/src/main/java/in/koreatech/business/feature/signup/completesignup/CompleteSignupSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/signup/completesignup/CompleteSignupSideEffect.kt new file mode 100644 index 000000000..d4c07c334 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/completesignup/CompleteSignupSideEffect.kt @@ -0,0 +1,6 @@ +package `in`.koreatech.business.feature.signup.completesignup + +sealed class CompleteSignupSideEffect { + data object NavigateToLoginScreen : CompleteSignupSideEffect() + data object NavigateToBackScreen : CompleteSignupSideEffect() +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/completesignup/CompleteSignupViewModel.kt b/business/src/main/java/in/koreatech/business/feature/signup/completesignup/CompleteSignupViewModel.kt new file mode 100644 index 000000000..e2503a5e3 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/signup/completesignup/CompleteSignupViewModel.kt @@ -0,0 +1,28 @@ +package `in`.koreatech.business.feature.signup.completesignup + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.viewmodel.container +import javax.inject.Inject + +@HiltViewModel +class CompleteSignupViewModel @Inject constructor() : + ContainerHost, ViewModel() { + override val container = + container(Unit) + + fun onNavigateToLoginScreen() { + intent { + postSideEffect(CompleteSignupSideEffect.NavigateToLoginScreen) + } + } + + fun onBackButtonClicked() { + intent { + postSideEffect(CompleteSignupSideEffect.NavigateToBackScreen) + } + } +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/dialog/BusinessAlertDialog.kt b/business/src/main/java/in/koreatech/business/feature/signup/dialog/BusinessAlertDialog.kt index f1216edbf..d9d6d5f95 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/dialog/BusinessAlertDialog.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/dialog/BusinessAlertDialog.kt @@ -1,5 +1,6 @@ package `in`.koreatech.business.feature.signup.dialog +import android.opengl.Visibility import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -39,8 +40,11 @@ fun BusinessAlertDialog( dialogTitle: String, dialogText: String, positiveButtonText: String, + visibility: Boolean = false ) { - + if (!visibility) { + return + } Dialog(onDismissRequest = { onDismissRequest() }) { Card( modifier = Modifier diff --git a/business/src/main/java/in/koreatech/business/feature/signup/navigator/SignupNavigator.kt b/business/src/main/java/in/koreatech/business/feature/signup/navigator/SignupNavigator.kt index 54f25af33..287b35124 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/navigator/SignupNavigator.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/navigator/SignupNavigator.kt @@ -4,55 +4,50 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.navigation.NavType +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import `in`.koreatech.business.feature.signup.accountauth.AccountAuthScreen import `in`.koreatech.business.feature.signup.accountsetup.AccountSetupScreen +import `in`.koreatech.business.feature.signup.accountsetup.AccountSetupViewModel import `in`.koreatech.business.feature.signup.businessauth.BusinessAuthScreen +import `in`.koreatech.business.feature.signup.businessauth.BusinessAuthViewModel import `in`.koreatech.business.feature.signup.businessauth.SearchStoreScreen +import `in`.koreatech.business.feature.signup.checkterm.CheckTermScreen import `in`.koreatech.business.feature.signup.completesignup.CompleteSignupScreen @Composable -fun SignupNavigator(modifier: Modifier) { +fun SignupNavigator( + modifier: Modifier, + accountSetupViewModel: AccountSetupViewModel = hiltViewModel(), + businessAuthViewModel: BusinessAuthViewModel = hiltViewModel(), +) { val navController = rememberNavController() NavHost( navController = navController, - startDestination = SignupRoute.BASIC_INFO_INPUT.name, - modifier = Modifier.padding(16.dp) + startDestination = SignupRoute.TERMS_OF_SERVICE.name, + modifier = modifier.padding(16.dp) ) { composable( - route = SignupRoute.BASIC_INFO_INPUT.name, + route = SignupRoute.TERMS_OF_SERVICE.name, ) { - AccountSetupScreen( + CheckTermScreen( onBackClicked = { navController.popBackStack() }, - onNextClicked = { - navController.navigate("${SignupRoute.ACCOUNT_AUTH.name}/$it") - }, + onNextClicked = { navController.navigate(SignupRoute.BASIC_INFO_INPUT.name) } ) } composable( - route = "${SignupRoute.ACCOUNT_AUTH.name}/{email}", - - arguments = listOf( - navArgument("email") { - type = NavType.StringType - nullable = true - } - ) + route = SignupRoute.BASIC_INFO_INPUT.name, ) { - val email = it.arguments?.getString("email") ?: "" - AccountAuthScreen( - email = email, + AccountSetupScreen( onBackClicked = { navController.popBackStack() }, onNextClicked = { navController.navigate(SignupRoute.BUSINESS_AUTH.name) }, + viewModel = accountSetupViewModel ) } @@ -60,24 +55,28 @@ fun SignupNavigator(modifier: Modifier) { route = SignupRoute.BUSINESS_AUTH.name, ) { BusinessAuthScreen( + accountSetupViewModel = accountSetupViewModel, + businessAuthViewModel = businessAuthViewModel, onBackClicked = { navController.popBackStack() }, onSearchClicked = { navController.navigate(SignupRoute.STORE_SETUP.name) }, - onNextClicked = { - navController.navigate(SignupRoute.SIGNUP_COMPLETED.name) - }, - ) + ) { + navController.navigate(SignupRoute.SIGNUP_COMPLETED.name) + } } composable( route = SignupRoute.STORE_SETUP.name, ) { SearchStoreScreen( - onBackClicked = { navController.popBackStack() }) + businessAuthViewModel = businessAuthViewModel, + onBackButtonClicked = { + navController.popBackStack() + }, + ) } - composable( route = SignupRoute.SIGNUP_COMPLETED.name, ) { @@ -85,7 +84,5 @@ fun SignupNavigator(modifier: Modifier) { onBackClicked = { navController.navigate(SignupRoute.LOGIN.name) } ) } - - } } \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/signup/navigator/SignupRoute.kt b/business/src/main/java/in/koreatech/business/feature/signup/navigator/SignupRoute.kt index 3942fb0f8..c6a3f107d 100644 --- a/business/src/main/java/in/koreatech/business/feature/signup/navigator/SignupRoute.kt +++ b/business/src/main/java/in/koreatech/business/feature/signup/navigator/SignupRoute.kt @@ -4,7 +4,7 @@ enum class SignupRoute(){ LOGIN, TERMS_OF_SERVICE, BASIC_INFO_INPUT, - ACCOUNT_AUTH, + EMAIL_AUTH, BUSINESS_AUTH, STORE_SETUP, SIGNUP_COMPLETED diff --git a/business/src/main/java/in/koreatech/business/feature/textfield/AuthTextField.kt b/business/src/main/java/in/koreatech/business/feature/textfield/AuthTextField.kt deleted file mode 100644 index db3fc659a..000000000 --- a/business/src/main/java/in/koreatech/business/feature/textfield/AuthTextField.kt +++ /dev/null @@ -1,84 +0,0 @@ -package `in`.koreatech.business.feature.textfield - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box -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.foundation.text.BasicTextField -import androidx.compose.material.Text -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.focus.onFocusChanged -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import `in`.koreatech.business.R -import `in`.koreatech.business.ui.theme.ColorError -import `in`.koreatech.business.ui.theme.ColorHelper -import `in`.koreatech.business.ui.theme.ColorSecondary - - -@Composable -fun AuthTextField( - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - label: String, - textStyle: TextStyle = TextStyle.Default.copy(fontSize = 15.sp), - isPassword: Boolean = false, - isError: Boolean = true, -) { - var focused by remember { mutableStateOf(false) } - - BasicTextField( - value = value, - onValueChange = onValueChange, - textStyle = textStyle, - modifier = modifier.onFocusChanged { focused = it.isFocused }, - maxLines = 1, - visualTransformation = if (isPassword) PasswordVisualTransformation() else VisualTransformation.None, - decorationBox = { innerTextField -> - Column { - Spacer(modifier = Modifier.height(12.dp)) - Box { - if (value.isEmpty()) { - Text(label, fontSize = 15.sp, color = ColorHelper) - } - innerTextField() - Canvas( - modifier = Modifier - .fillMaxWidth() - .height(30.dp) - ) { - drawLine( - color = if (value.isEmpty()) ColorHelper else if (isError) ColorError - else ColorSecondary, - start = Offset(0f, size.height), - end = Offset(size.width, size.height), - strokeWidth = 1.dp.toPx(), - cap = StrokeCap.Round - ) - } - - } - - if (isError) Text( - text = stringResource(id = R.string.verification_code_error), - fontSize = 11.sp, - color = ColorError - ) - } - } - ) -} diff --git a/business/src/main/java/in/koreatech/business/feature/textfield/LinedTextField.kt b/business/src/main/java/in/koreatech/business/feature/textfield/LinedTextField.kt index e9ca72326..d9c4c28b2 100644 --- a/business/src/main/java/in/koreatech/business/feature/textfield/LinedTextField.kt +++ b/business/src/main/java/in/koreatech/business/feature/textfield/LinedTextField.kt @@ -1,25 +1,40 @@ package `in`.koreatech.business.feature.textfield import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.OutlinedTextField import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults 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.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import `in`.koreatech.business.ui.theme.ColorError import `in`.koreatech.business.ui.theme.ColorHelper +import `in`.koreatech.business.ui.theme.ColorPrimary +import `in`.koreatech.business.ui.theme.ColorSecondary +import `in`.koreatech.business.ui.theme.ColorTextField @Composable @@ -29,44 +44,44 @@ fun LinedTextField( modifier: Modifier = Modifier, label: String, textStyle: TextStyle = TextStyle.Default.copy(fontSize = 15.sp), - lineColor: Color = ColorHelper, - filledLineColor: Color = Color.Black, isPassword: Boolean = false, helperText: String = "", errorText: String = "", isError: Boolean = false, ) { + var focused by remember { mutableStateOf(false) } BasicTextField( value = value, onValueChange = onValueChange, textStyle = textStyle, - modifier = modifier, + modifier = modifier.onFocusChanged { focused = it.isFocused }, maxLines = 1, - visualTransformation = if (isPassword) PasswordVisualTransformation() else VisualTransformation.None, decorationBox = { innerTextField -> Column(modifier = Modifier.fillMaxWidth()) { - Spacer(modifier = Modifier.height(12.dp)) - Box { - if (value.isEmpty()) { - Text(label, fontSize = 15.sp, color = ColorHelper) - } - innerTextField() - Canvas( - modifier = Modifier - .fillMaxWidth() - .height(30.dp) - ) { - drawLine( - strokeWidth = 1.dp.toPx(), - color = if (value.isNotEmpty()) filledLineColor else if (isError) ColorError - else lineColor, - start = Offset(0f, size.height), - end = Offset(size.width, size.height), - cap = StrokeCap.Round + + Column( + modifier = Modifier + .fillMaxWidth() + .height(41.dp) + .border( + width = 1.dp, + color = if (isError) ColorSecondary else if (focused) ColorPrimary else ColorTextField, + shape = RoundedCornerShape(4.dp) ) + .background(color = ColorTextField, shape = RoundedCornerShape(4.dp)), + verticalArrangement = Arrangement.Center, + ) { + Box( + modifier = Modifier.padding(start = 12.dp), + ) { + if(value.isEmpty()) + Text(text = label, fontSize = 14.sp, color = ColorHelper, ) + innerTextField() } + } + Box { Text( modifier = Modifier, @@ -74,10 +89,12 @@ fun LinedTextField( fontSize = 11.sp, color = ColorHelper, ) - if (isError) Text(text = errorText, fontSize = 11.sp, color = ColorError) + if (isError) Text(text = errorText, fontSize = 11.sp, color = ColorSecondary) } + } + } ) } diff --git a/business/src/main/java/in/koreatech/business/feature/textfield/SearchTextField.kt b/business/src/main/java/in/koreatech/business/feature/textfield/SearchTextField.kt new file mode 100644 index 000000000..2b4088c7b --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/textfield/SearchTextField.kt @@ -0,0 +1,78 @@ +package `in`.koreatech.business.feature.textfield + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import `in`.koreatech.business.R +import `in`.koreatech.business.ui.theme.ColorHelper +import `in`.koreatech.business.ui.theme.ColorTextField +import `in`.koreatech.business.ui.theme.Gray5 +import `in`.koreatech.business.ui.theme.Gray6 + + +@Composable +fun SearchTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + label: String, + textStyle: TextStyle = TextStyle.Default.copy(fontSize = 15.sp, color = Color.Black), + isPassword: Boolean = false, +) { + BasicTextField( + modifier = modifier, + value = value, + onValueChange = onValueChange, + textStyle = textStyle.copy(color = Color.Black), + maxLines = 1, + visualTransformation = if (isPassword) PasswordVisualTransformation() else VisualTransformation.None, + decorationBox = { innerTextField -> + Row( + modifier = Modifier.fillMaxWidth().height(40.dp) + .background(color = Gray5, shape = RoundedCornerShape(4.dp)) + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Box { + if (value.isEmpty()) { + Text(label, fontSize = 15.sp, color = ColorHelper) + } + innerTextField() + } + IconButton(onClick = { }) { + Icon( + modifier = Modifier.size(17.dp), + painter = painterResource(id = R.drawable.search), + contentDescription = stringResource(id = R.string.search_icon) + ) + } + } + + } + ) +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature_changepassword/changepassword/ChangePasswordViewModel.kt b/business/src/main/java/in/koreatech/business/feature_changepassword/changepassword/ChangePasswordViewModel.kt index 3a5b26b73..f54eac56f 100644 --- a/business/src/main/java/in/koreatech/business/feature_changepassword/changepassword/ChangePasswordViewModel.kt +++ b/business/src/main/java/in/koreatech/business/feature_changepassword/changepassword/ChangePasswordViewModel.kt @@ -6,9 +6,6 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import `in`.koreatech.koin.domain.state.business.changepw.ChangePasswordExceptionState import `in`.koreatech.koin.domain.usecase.business.OwnerChangePasswordUseCase -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost diff --git a/business/src/main/java/in/koreatech/business/feature_changepassword/passwordauthentication/PasswordAuthenticationSideEffect.kt b/business/src/main/java/in/koreatech/business/feature_changepassword/passwordauthentication/PasswordAuthenticationSideEffect.kt index 59a21fe3b..dbd9d92bd 100644 --- a/business/src/main/java/in/koreatech/business/feature_changepassword/passwordauthentication/PasswordAuthenticationSideEffect.kt +++ b/business/src/main/java/in/koreatech/business/feature_changepassword/passwordauthentication/PasswordAuthenticationSideEffect.kt @@ -5,9 +5,7 @@ import `in`.koreatech.koin.domain.state.business.changepw.ChangePasswordExceptio sealed class PasswordAuthenticationSideEffect { data class GotoChangePasswordScreen(val email: String): PasswordAuthenticationSideEffect() - object SendAuthCode: PasswordAuthenticationSideEffect() - data class ShowMessage(val type: ErrorType): PasswordAuthenticationSideEffect() } diff --git a/business/src/main/java/in/koreatech/business/ui/theme/Color.kt b/business/src/main/java/in/koreatech/business/ui/theme/Color.kt index 5b4659f47..d452ad0ee 100644 --- a/business/src/main/java/in/koreatech/business/ui/theme/Color.kt +++ b/business/src/main/java/in/koreatech/business/ui/theme/Color.kt @@ -15,7 +15,12 @@ val ColorAccent = Color(0xFFF7941E) val ColorSecondaryText = Color(0xFFa1a1a1) val Black1 = Color(0xFF222222) +val Gray6= Color(0xFF8E8E8E) val Gray5 = Color(0xFFC4C4C4) +val Gray1 = Color(0xFF4B4B4B) +val Gray2= Color(0xFFEEEEEE) +val Gray3= Color(0xFFCACACA) +val ColorTextField= Color(0xFFF5F5F5) val Red2 = Color(0xFFFF0000) val ColorError = Color(0xFFF05D3D) @@ -25,7 +30,6 @@ val Blue1 = Color(0xFFD2DAE2) val ColorPrimary = Color(0xFF175c8e) - val ColorActiveButton = Color(0xFF175c8e) val ColorDisabledButton = Color(0xFFC4C4C4) val ColorSecondary = Color(0xFFF7941E) diff --git a/business/src/main/java/in/koreatech/business/ui/theme/Theme.kt b/business/src/main/java/in/koreatech/business/ui/theme/Theme.kt index 4a2a78aee..d10516845 100644 --- a/business/src/main/java/in/koreatech/business/ui/theme/Theme.kt +++ b/business/src/main/java/in/koreatech/business/ui/theme/Theme.kt @@ -49,7 +49,6 @@ fun KOIN_ANDROIDTheme( MaterialTheme( colors = colorScheme, - typography = Typography, shapes = Shapes, content = content ) diff --git a/business/src/main/java/in/koreatech/business/util/OwnerTokenAuthenticator.kt b/business/src/main/java/in/koreatech/business/util/OwnerTokenAuthenticator.kt new file mode 100644 index 000000000..e71031b8a --- /dev/null +++ b/business/src/main/java/in/koreatech/business/util/OwnerTokenAuthenticator.kt @@ -0,0 +1,74 @@ +package `in`.koreatech.business.util + +import android.content.Context +import android.content.Intent +import android.os.Looper +import android.widget.Toast +import androidx.core.os.HandlerCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import `in`.koreatech.business.BusinessMainActivity +import `in`.koreatech.business.R +import `in`.koreatech.business.feature.signup.businessauth.BusinessAuthState +import `in`.koreatech.koin.data.source.local.TokenLocalDataSource + +import kotlinx.coroutines.runBlocking +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import java.net.HttpURLConnection +import javax.inject.Inject + +class OwnerTokenAuthenticator @Inject constructor( + @ApplicationContext private val context: Context, + private val tokenLocalDataSource: TokenLocalDataSource +): Authenticator { + override fun authenticate(route: Route?, response: Response): Request? = runBlocking { + val request = try { + if(response.code != HttpURLConnection.HTTP_UNAUTHORIZED) null + else { + val accessToken = tokenLocalDataSource.getOwnerAccessToken() + if(accessToken == response.request.header("OwnerAuthorization")) { + tokenLocalDataSource.removeAccessToken() + goToLoginActivity() + null + } else { + if(accessToken.isNullOrEmpty()) { + goToLoginActivity() + null + } else { + getRequest(response, accessToken) + } + } + } + } catch (e: Exception) { + goToLoginActivity() + null + } + + request + } + + private fun getRequest(response: Response, token: String): Request { + return response.request + .newBuilder() + .removeHeader("OwnerAuthorization") + .addHeader("OwnerAuthorization", "Bearer $token") + .build() + } + + private fun goToLoginActivity() { + val handler = HandlerCompat.createAsync(Looper.getMainLooper()) + Intent(context.applicationContext, BusinessMainActivity::class.java).run { + handler.post { + Toast.makeText( + context.applicationContext, + context.getString(R.string.token_out_dated), + Toast.LENGTH_SHORT + ).show() + } + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + context.startActivity(this) + } + } +} \ No newline at end of file diff --git a/business/src/main/res/drawable/attach_file_add.xml b/business/src/main/res/drawable/attach_file_add.xml new file mode 100644 index 000000000..e0296e60f --- /dev/null +++ b/business/src/main/res/drawable/attach_file_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/business/src/main/res/drawable/complete_signup.xml b/business/src/main/res/drawable/complete_signup.xml new file mode 100644 index 000000000..f6e56483b --- /dev/null +++ b/business/src/main/res/drawable/complete_signup.xml @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/business/src/main/res/drawable/ic_arrow_back.xml b/business/src/main/res/drawable/ic_back.xml similarity index 100% rename from business/src/main/res/drawable/ic_arrow_back.xml rename to business/src/main/res/drawable/ic_back.xml diff --git a/business/src/main/res/drawable/ic_check.xml b/business/src/main/res/drawable/ic_check.xml new file mode 100644 index 000000000..46db90d01 --- /dev/null +++ b/business/src/main/res/drawable/ic_check.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/business/src/main/res/drawable/ic_check_selected.xml b/business/src/main/res/drawable/ic_check_selected.xml new file mode 100644 index 000000000..68be0a2c5 --- /dev/null +++ b/business/src/main/res/drawable/ic_check_selected.xml @@ -0,0 +1,9 @@ + + + diff --git a/business/src/main/res/drawable/ic_delete_button.xml b/business/src/main/res/drawable/ic_delete_button.xml new file mode 100644 index 000000000..a6ebff22c --- /dev/null +++ b/business/src/main/res/drawable/ic_delete_button.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/business/src/main/res/values/strings.xml b/business/src/main/res/values/strings.xml index be24bb85c..8c8b73559 100644 --- a/business/src/main/res/values/strings.xml +++ b/business/src/main/res/values/strings.xml @@ -3,6 +3,17 @@ 사장님용\n회원가입 + 이메일 입력 + 인증번호 입력 + 비밀번호 찾기 + 비밀번호 변경 + 비밀번호가 일치하지 않습니다. + 비밀번호가 변경되었습니다.\n새로운 비밀번호로 로그인 부탁드립니다. + 재발송 + 인증번호 발송 + 비밀번호 확인 + 새 비밀번호 + 비밀번호 변경하기 로그인 화면 바로가기 backArrow 오류가 발생하였습니다. 잠시뒤에 다시 시도해 주세요 @@ -34,7 +45,8 @@ 비밀번호 찾기 비밀번호 변경 - 1.기본 정보 입력 + 1. 약관 동의 + 2.기본 정보 입력 아이디 비밀번호 비밀번호가 일치하지 않습니다. @@ -43,7 +55,7 @@ 이메일 인증하기 계정 인증 발송된 인증번호 6자리를 입력해주세요 - 인증번호 입력 + 인증번호 입력를 입력해주세요. 재발송 @@ -57,29 +69,55 @@ 3.사업자 인증 대표자명(실명) 가게명 직접 입력하기 - 가게검색 - 파일첨부 + 가게 검색 + 파일 첨부 파일을 첨부해주세요 - 사업자등록증, 영업신고증, 통장사본 이미지 첨부 - 사업자등록증, 영업신고증, 통장사본 이미지 필수\\n10mb 이하의 PDF 혹은 이미지 형식의 파일(e.g. jpg, png, gif 등)로 5개까지 업로드 가능합니다. + 사업자 등록증, 영업신고증, 통장사본을 첨부하세요. + 사업자등록증, 영업신고증, 통장사본 이미지 필수\n10mb 이하의 PDF 혹은 이미지 형식의 파일(e.g. jpg, png, gif 등)로 5개까지 업로드 가능합니다. 파일 선택하기 취소 사업자 등록번호 개인 연락처 - 상점 검색 + 가게를 검색해보세요. 식당이름 배달 카드결제 계좌이체 - 전화번호 + 휴대폰 번호 선택 - 가입 신청 완료 - 가입 신청이 완료되었습니다.\\n가입 허가가 승인되면 로그인이 가능합니다. - 로그인 화면으로 가기 + 회원가입 완료 + 회원가입이 완료되었습니다!\n가입 승인 시 로그인이 가능합니다. + 로그인 화면 바로가기 BackArrow check upload file upload item image search 존재하지 않는 도메인입니다. + 아이디를 입력해주세요. + 비밀번호를 입력해주세요. + 비밀번호를 다시 입력해주세요. + -없이 번호를 입력해주세요. + 회원가입 + 올바르지 않는 사업자 번호입니다.. + 존재하지 않는 휴대폰 번호입니다. + 중복 확인 + 인증번호 + 중복된 아이디입니다. + 가게명 + 이름을 입력해주세요. + 사업자 등록번호를 입력해주세요. + -없이 번호를 입력해주세요. + 사업자 인증 파일 + 인증번호가 일치하지 않습니다. + 중복된 휴대폰 번호입니다. + 특수문자 포함 영어와 숫자 6~18 자리로 입력해주세요. + 파일 첨부 + 모두 동의 합니다. + 제1조 (목적) 코인 서비스 이용약관은 bcsd lab에서 서비스를 제공함에 있어, 이용자간의 관리, 의무 및 책임 사항 등을 목적으로 합니다. \n제2조 (약관의 효력 및 변경) ① 이 약관은 서비스 화면이나 기타의 방법으로 이용고객에게 공지함으로써 효력을 발생합니다. ② 사이트는 이 약관의 내용을 변경할 수 있으며, 변경된 약관은 제1항과 같은 방법으로 공지 또는 통지함으로써 효력을 발생합니다. + 제1조 (목적) 코인 서비스 이용약관은 bcsd lab에서 서비스를 제공함에 있어, 이용자간의 관리, 의무 및 책임 사항 등을 목적으로 합니다. \n제2조 (약관의 효력 및 변경) ① 이 약관은 서비스 화면이나 기타의 방법으로 이용고객에게 공지함으로써 효력을 발생합니다. ② 사이트는 이 약관의 내용을 변경할 수 있으며, 변경된 약관은 제1항과 같은 방법으로 공지 또는 통지함으로써 효력을 발생합니다. + 코인 이용약관 (필수) + 개인정보 이용약관 (필수) + 확인 + diff --git a/core/src/main/java/in/koreatech/koin/core/dialog/ZoomableDialog.kt b/core/src/main/java/in/koreatech/koin/core/dialog/ImageZoomableDialog.kt similarity index 100% rename from core/src/main/java/in/koreatech/koin/core/dialog/ZoomableDialog.kt rename to core/src/main/java/in/koreatech/koin/core/dialog/ImageZoomableDialog.kt diff --git a/core/src/main/res/drawable/ic_edit.xml b/core/src/main/res/drawable/ic_edit.xml new file mode 100644 index 000000000..178a06432 --- /dev/null +++ b/core/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,20 @@ + + + + diff --git a/core/src/main/res/drawable/ic_no_store_image.xml b/core/src/main/res/drawable/ic_no_store_image.xml new file mode 100644 index 000000000..9d7f132db --- /dev/null +++ b/core/src/main/res/drawable/ic_no_store_image.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index e97e247f1..6dc084360 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -10,6 +10,11 @@ 확인 아니오 취소 + 다음 + 1 / 4 + 2 / 4 + 3 / 4 + 4 / 4 @@ -77,4 +82,20 @@ 님, 안녕하세요! 내 정보 햄버거 + + + 가게 등록 + 가게 정보 기입 + 가게의 다양한 정보를 입력 및 수정하여\n학생들에게 최신 가게 정보를 알려주세요. + 가게 정보 기입하기 + 등록 하시려는 업체의\n메인 정보를 입력해 주세요. + 1. 가게 카테고리 설정 + 카테고리를 골라주세요. + + 2. 기본 정보 입력 + 가게이름 + 주소정보 + 가게이름을 입력해주세요. + 주소정보를 입력해주세요. + 가게사진을 등록해주세요. diff --git a/data/src/main/java/in/koreatech/koin/data/api/OwnerApi.kt b/data/src/main/java/in/koreatech/koin/data/api/OwnerApi.kt index bb069fec2..82c7e28c0 100644 --- a/data/src/main/java/in/koreatech/koin/data/api/OwnerApi.kt +++ b/data/src/main/java/in/koreatech/koin/data/api/OwnerApi.kt @@ -5,6 +5,8 @@ import `in`.koreatech.koin.data.request.owner.OwnerChangePasswordRequest import `in`.koreatech.koin.data.request.owner.OwnerRegisterRequest import `in`.koreatech.koin.data.request.owner.OwnerVerificationCodeRequest import `in`.koreatech.koin.data.request.owner.OwnerVerificationEmailRequest +import `in`.koreatech.koin.data.request.owner.VerificationCodeSmsRequest +import `in`.koreatech.koin.data.request.owner.VerificationSmsRequest import `in`.koreatech.koin.data.response.owner.OwnerResponse import `in`.koreatech.koin.data.response.owner.OwnerVerificationCodeResponse import retrofit2.Response @@ -30,4 +32,9 @@ interface OwnerApi { @PUT(URLConstant.OWNER.CHANGEPASSWORD) suspend fun changePassword(@Body ownerChangePasswordRequest: OwnerChangePasswordRequest) + @POST(URLConstant.OWNER.SMS) + suspend fun postVerificationSms(@Body ownerVerificationSms: VerificationSmsRequest) + + @POST(URLConstant.OWNER.CODE_SMS) + suspend fun postVerificationCodeSms(@Body ownerVerificationCode: VerificationCodeSmsRequest): OwnerVerificationCodeResponse } \ No newline at end of file diff --git a/data/src/main/java/in/koreatech/koin/data/constant/URLConstant.kt b/data/src/main/java/in/koreatech/koin/data/constant/URLConstant.kt index e62567132..628de3080 100644 --- a/data/src/main/java/in/koreatech/koin/data/constant/URLConstant.kt +++ b/data/src/main/java/in/koreatech/koin/data/constant/URLConstant.kt @@ -71,6 +71,9 @@ object URLConstant { const val CHANGEPASSWORDEMAIL = "$OWNERS/$PASSWORD/$RESET/$VERIFICATION" const val CHANGEPASSWORDCODE = "$OWNERS/$PASSWORD/$RESET/send" const val CHANGEPASSWORD ="$OWNERS/$PASSWORD/$RESET" + const val CODE_SMS = "$OWNERS/$VERIFICATION/code/sms" + const val SMS = "$OWNERS/$VERIFICATION/sms" + const val PW = "password" } object CALLVANS { diff --git a/data/src/main/java/in/koreatech/koin/data/mapper/ApiMapper.kt b/data/src/main/java/in/koreatech/koin/data/mapper/ApiMapper.kt new file mode 100644 index 000000000..b69015b76 --- /dev/null +++ b/data/src/main/java/in/koreatech/koin/data/mapper/ApiMapper.kt @@ -0,0 +1,13 @@ +package `in`.koreatech.koin.data.mapper + +import kotlin.coroutines.cancellation.CancellationException + +suspend fun safeApiCall(call: suspend () -> T): Result { + return try { + Result.success(call()) + } catch (e: CancellationException) { + throw e + } catch (t: Throwable) { + Result.failure(t) + } +} diff --git a/data/src/main/java/in/koreatech/koin/data/repository/DeptRepositoryImpl.kt b/data/src/main/java/in/koreatech/koin/data/repository/DeptRepositoryImpl.kt index 0f404fa6e..67047d818 100644 --- a/data/src/main/java/in/koreatech/koin/data/repository/DeptRepositoryImpl.kt +++ b/data/src/main/java/in/koreatech/koin/data/repository/DeptRepositoryImpl.kt @@ -9,7 +9,7 @@ import javax.inject.Inject class DeptRepositoryImpl @Inject constructor( private val deptRemoteDataSource: DeptRemoteDataSource, private val deptLocalDataSource: DeptLocalDataSource -) : DeptRepository{ +) : DeptRepository { override suspend fun getDeptNameFromDeptCode(deptCode: String): String { return try { val deptResponse = deptRemoteDataSource.getDeptFromDeptCode(deptCode) @@ -28,4 +28,14 @@ class DeptRepositoryImpl @Inject constructor( ) } } + + override suspend fun getDeptNames(): List { + return try { + deptRemoteDataSource.getAllDepts().map { + it.name + } + } catch (t: Throwable) { + deptLocalDataSource.getDeptNames() + } + } } \ No newline at end of file diff --git a/data/src/main/java/in/koreatech/koin/data/repository/OwnerRegisterRepositoryImpl.kt b/data/src/main/java/in/koreatech/koin/data/repository/OwnerRegisterRepositoryImpl.kt index 7741ab552..b1b324fd8 100644 --- a/data/src/main/java/in/koreatech/koin/data/repository/OwnerRegisterRepositoryImpl.kt +++ b/data/src/main/java/in/koreatech/koin/data/repository/OwnerRegisterRepositoryImpl.kt @@ -18,7 +18,7 @@ class OwnerRegisterRepositoryImpl( name: String, password: String, phoneNumber: String, - shopId: Int, + shopId: Int?, shopName: String ): Result { return try { diff --git a/data/src/main/java/in/koreatech/koin/data/repository/OwnerSignupRepositoryImpl.kt b/data/src/main/java/in/koreatech/koin/data/repository/OwnerSignupRepositoryImpl.kt index 508fc7709..b92187167 100644 --- a/data/src/main/java/in/koreatech/koin/data/repository/OwnerSignupRepositoryImpl.kt +++ b/data/src/main/java/in/koreatech/koin/data/repository/OwnerSignupRepositoryImpl.kt @@ -1,17 +1,20 @@ package `in`.koreatech.koin.data.repository import `in`.koreatech.koin.data.mapper.httpExceptionMapper +import `in`.koreatech.koin.data.mapper.safeApiCall import `in`.koreatech.koin.data.request.owner.OwnerVerificationEmailRequest +import `in`.koreatech.koin.data.request.owner.VerificationSmsRequest import `in`.koreatech.koin.data.source.local.SignupTermsLocalDataSource import `in`.koreatech.koin.data.source.remote.OwnerRemoteDataSource import `in`.koreatech.koin.domain.repository.OwnerSignupRepository import retrofit2.HttpException import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException class OwnerSignupRepositoryImpl @Inject constructor( private val ownerRemoteDataSource: OwnerRemoteDataSource, private val signupTermsLocalDataSource: SignupTermsLocalDataSource -): OwnerSignupRepository { +) : OwnerSignupRepository { override suspend fun getPrivacyTermText(): String { return signupTermsLocalDataSource.getPrivacyTermText() } @@ -24,7 +27,7 @@ class OwnerSignupRepositoryImpl @Inject constructor( email: String ): Result { return try { - ownerRemoteDataSource.postVerificationEmail ( + ownerRemoteDataSource.postVerificationEmail( OwnerVerificationEmailRequest( address = email ) @@ -38,4 +41,15 @@ class OwnerSignupRepositoryImpl @Inject constructor( } } + override suspend fun requestSmsVerificationCode( + phoneNumber: String + ): Result { + return safeApiCall { + ownerRemoteDataSource.postVerificationSms( + VerificationSmsRequest( + phoneNumber = phoneNumber + ) + ) + } + } } \ No newline at end of file diff --git a/data/src/main/java/in/koreatech/koin/data/repository/OwnerVerificationCodeRepositoryImpl.kt b/data/src/main/java/in/koreatech/koin/data/repository/OwnerVerificationCodeRepositoryImpl.kt index bda29bf68..d2874577c 100644 --- a/data/src/main/java/in/koreatech/koin/data/repository/OwnerVerificationCodeRepositoryImpl.kt +++ b/data/src/main/java/in/koreatech/koin/data/repository/OwnerVerificationCodeRepositoryImpl.kt @@ -1,10 +1,10 @@ package `in`.koreatech.koin.data.repository -import `in`.koreatech.koin.data.mapper.httpExceptionMapper +import `in`.koreatech.koin.data.mapper.safeApiCall import `in`.koreatech.koin.data.mapper.toAuthToken import `in`.koreatech.koin.data.request.owner.OwnerVerificationCodeRequest +import `in`.koreatech.koin.data.request.owner.VerificationCodeSmsRequest import `in`.koreatech.koin.data.source.remote.OwnerRemoteDataSource -import `in`.koreatech.koin.domain.error.owner.OwnerError import `in`.koreatech.koin.domain.model.owner.OwnerAuthToken import `in`.koreatech.koin.domain.repository.OwnerVerificationCodeRepository import retrofit2.HttpException @@ -12,7 +12,7 @@ import javax.inject.Inject class OwnerVerificationCodeRepositoryImpl @Inject constructor( private val ownerRemoteDataSource: OwnerRemoteDataSource -):OwnerVerificationCodeRepository { +) : OwnerVerificationCodeRepository { override suspend fun compareVerificationCode( address: String, verificationCode: String @@ -33,4 +33,18 @@ class OwnerVerificationCodeRepositoryImpl @Inject constructor( } } + override suspend fun verifySmsCode( + phoneNumber: String, + verificationCode: String + ): Result { + return safeApiCall { + val tempToken = ownerRemoteDataSource.postVerificationCodeSms( + VerificationCodeSmsRequest( + phoneNumber = phoneNumber, + certificationCode = verificationCode + ) + ) + tempToken.toAuthToken() + } + } } \ No newline at end of file diff --git a/data/src/main/java/in/koreatech/koin/data/repository/PreSignedUrlRepositoryImpl.kt b/data/src/main/java/in/koreatech/koin/data/repository/PreSignedUrlRepositoryImpl.kt index 9ffd7b04a..9502b24d5 100644 --- a/data/src/main/java/in/koreatech/koin/data/repository/PreSignedUrlRepositoryImpl.kt +++ b/data/src/main/java/in/koreatech/koin/data/repository/PreSignedUrlRepositoryImpl.kt @@ -1,17 +1,25 @@ package `in`.koreatech.koin.data.repository +import `in`.koreatech.koin.data.mapper.safeApiCall import `in`.koreatech.koin.data.requestbody.S3RequestBody import `in`.koreatech.koin.data.source.remote.PreSignedUrlRemoteDataSource import `in`.koreatech.koin.domain.repository.PreSignedUrlRepository import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody import retrofit2.HttpException import java.io.InputStream import javax.inject.Inject class PreSignedUrlRepositoryImpl @Inject constructor( private val preSignedUrlRemoteDataSource: PreSignedUrlRemoteDataSource -): PreSignedUrlRepository { - override suspend fun putPreSignedUrl(url: String, inputStream: InputStream, mediaType: String, mediaSize: Long): Result { +) : PreSignedUrlRepository { + override suspend fun putPreSignedUrl( + url: String, + inputStream: InputStream, + mediaType: String, + mediaSize: Long + ): Result { return try { val file = S3RequestBody(inputStream, mediaType.toMediaType(), mediaSize) preSignedUrlRemoteDataSource.putPreSignedUrl(url, file) @@ -24,4 +32,15 @@ class PreSignedUrlRepositoryImpl @Inject constructor( } } -} \ No newline at end of file + override suspend fun uploadFile( + url: String, + bitmap: String, + mediaType: String, + mediaSize: Long + ): Result { + return safeApiCall { + val file = bitmap.toRequestBody(mediaType.toMediaTypeOrNull()) + preSignedUrlRemoteDataSource.putPreSignedUrl(url, file) + } + } +} diff --git a/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerRegisterRequest.kt b/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerRegisterRequest.kt index 706c3346c..3992e8c91 100644 --- a/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerRegisterRequest.kt +++ b/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerRegisterRequest.kt @@ -3,12 +3,12 @@ package `in`.koreatech.koin.data.request.owner import com.google.gson.annotations.SerializedName data class OwnerRegisterRequest( - @SerializedName("attachment_urls") val attachmentUrls: List, - @SerializedName("company_number") val companyNumber: String, - @SerializedName("email") val email: String, - @SerializedName("name") val name: String, - @SerializedName("password") val password: String, - @SerializedName("phone_number") val phoneNumber: String, - @SerializedName("shop_id") val shopId: Int, - @SerializedName("shop_name") val shopName: String + @SerializedName("attachment_urls") val attachmentUrls: List?, + @SerializedName("company_number") val companyNumber: String?, + @SerializedName("email") val email: String?, + @SerializedName("name") val name: String?, + @SerializedName("password") val password: String?, + @SerializedName("phone_number") val phoneNumber: String?, + @SerializedName("shop_id") val shopId: Int?, + @SerializedName("shop_name") val shopName: String? ) \ No newline at end of file diff --git a/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerVerificationCodeRequest.kt b/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerVerificationCodeRequest.kt index 3910746bd..f82c699f1 100644 --- a/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerVerificationCodeRequest.kt +++ b/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerVerificationCodeRequest.kt @@ -6,3 +6,8 @@ data class OwnerVerificationCodeRequest( @SerializedName("address") val address: String, @SerializedName("certification_code") val certificationCode: String ) + +data class VerificationCodeSmsRequest( + @SerializedName("phone_number") val phoneNumber: String, + @SerializedName("certification_code") val certificationCode: String +) diff --git a/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerVerificationEmailRequest.kt b/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerVerificationEmailRequest.kt index 104f1b7ba..3a7b061c6 100644 --- a/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerVerificationEmailRequest.kt +++ b/data/src/main/java/in/koreatech/koin/data/request/owner/OwnerVerificationEmailRequest.kt @@ -5,3 +5,7 @@ import com.google.gson.annotations.SerializedName data class OwnerVerificationEmailRequest( @SerializedName("address") val address: String ) + +data class VerificationSmsRequest( + @SerializedName("phone_number") val phoneNumber: String? +) \ No newline at end of file diff --git a/data/src/main/java/in/koreatech/koin/data/source/local/DeptLocalDataSource.kt b/data/src/main/java/in/koreatech/koin/data/source/local/DeptLocalDataSource.kt index f19a01a64..b384d797e 100644 --- a/data/src/main/java/in/koreatech/koin/data/source/local/DeptLocalDataSource.kt +++ b/data/src/main/java/in/koreatech/koin/data/source/local/DeptLocalDataSource.kt @@ -22,4 +22,17 @@ class DeptLocalDataSource @Inject constructor( else -> throw IllegalArgumentException() } ) + + fun getDeptNames() = with(context) { + listOf( + getString(R.string.major_architectural_engineering), + getString(R.string.major_employment_service_policy), + getString(R.string.major_mechanical_engineering), + getString(R.string.major_architectural_engineering), + getString(R.string.major_architectural_engineering), + getString(R.string.major_architectural_engineering), + getString(R.string.major_architectural_engineering), + getString(R.string.major_architectural_engineering), + ) + } } \ No newline at end of file diff --git a/data/src/main/java/in/koreatech/koin/data/source/remote/OwnerRemoteDataSource.kt b/data/src/main/java/in/koreatech/koin/data/source/remote/OwnerRemoteDataSource.kt index 97f755e97..5e15333b3 100644 --- a/data/src/main/java/in/koreatech/koin/data/source/remote/OwnerRemoteDataSource.kt +++ b/data/src/main/java/in/koreatech/koin/data/source/remote/OwnerRemoteDataSource.kt @@ -5,6 +5,8 @@ import `in`.koreatech.koin.data.request.owner.OwnerChangePasswordRequest import `in`.koreatech.koin.data.request.owner.OwnerRegisterRequest import `in`.koreatech.koin.data.request.owner.OwnerVerificationCodeRequest import `in`.koreatech.koin.data.request.owner.OwnerVerificationEmailRequest +import `in`.koreatech.koin.data.request.owner.VerificationCodeSmsRequest +import `in`.koreatech.koin.data.request.owner.VerificationSmsRequest import `in`.koreatech.koin.data.response.owner.OwnerResponse import `in`.koreatech.koin.data.response.owner.OwnerVerificationCodeResponse @@ -32,4 +34,12 @@ class OwnerRemoteDataSource(private val ownerApi: OwnerApi) { suspend fun ownerChangePassword(ownerChangePasswordRequest: OwnerChangePasswordRequest) { return ownerApi.changePassword(ownerChangePasswordRequest) } + + suspend fun postVerificationSms(ownerVerificationEmail: VerificationSmsRequest) { + return ownerApi.postVerificationSms(ownerVerificationEmail) + } + + suspend fun postVerificationCodeSms(ownerVerificationCode: VerificationCodeSmsRequest): OwnerVerificationCodeResponse { + return ownerApi.postVerificationCodeSms(ownerVerificationCode) + } } \ No newline at end of file diff --git a/data/src/main/res/values/strings.xml b/data/src/main/res/values/strings.xml index d59d863ed..7869208f7 100644 --- a/data/src/main/res/values/strings.xml +++ b/data/src/main/res/values/strings.xml @@ -4,6 +4,8 @@ 서버 오류가 발생했습니다. 네트워크 연결을 확인해 주세요. 알 수 없는 오류가 발생했습니다. + 아이디 또는 비밀번호가 올바르지 않습니다. + 서버 오류가 발생했습니다. 가입되지 않은 아이디입니다. @@ -43,6 +45,7 @@ 셔틀버스 학교셔틀 %1$d번 버스 + 기계공학부 메카트로닉스공학부 @@ -56,8 +59,19 @@ HRD 융합 교양학부 - 아이디 또는 비밀번호가 올바르지 않습니다. - 서버 오류가 발생했습니다. + + + 건축공학부 + 고용서비스정책학과 + 기계공학부 + 디자인공학부 + 메카트로닉스공학부 + 산업경영학부 + 전기전자통신공학부 + 컴퓨터공학부 + 화학생명공학부 + 에너지신소재화학공학부 + 아침 점심 diff --git a/domain/build.gradle b/domain/build.gradle index 89646d9ce..cd57be316 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -12,4 +12,5 @@ dependencies { implementation libs.kotlinx.coroutines.core implementation libs.javax.inject implementation libs.napier + } diff --git a/domain/src/main/java/in/koreatech/koin/domain/model/owner/insertstore/StoreBasicInfo.kt b/domain/src/main/java/in/koreatech/koin/domain/model/owner/insertstore/StoreBasicInfo.kt new file mode 100644 index 000000000..263735def --- /dev/null +++ b/domain/src/main/java/in/koreatech/koin/domain/model/owner/insertstore/StoreBasicInfo.kt @@ -0,0 +1,8 @@ +package `in`.koreatech.koin.domain.model.owner.insertstore + +data class StoreBasicInfo( + val storeName: String ="", + val storeAddress: String ="", + val storeImage: String ="", + val storeCategory: Int = -1 +) diff --git a/domain/src/main/java/in/koreatech/koin/domain/model/user/Dept.kt b/domain/src/main/java/in/koreatech/koin/domain/model/user/Dept.kt index 585ec28cc..acfd170a3 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/model/user/Dept.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/model/user/Dept.kt @@ -1,7 +1,7 @@ package `in`.koreatech.koin.domain.model.user data class Dept( - private val name: String, - private val curriculumUrl: String, - private val codes: List + val name: String, + val curriculumUrl: String, + val codes: List ) \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/repository/DeptRepository.kt b/domain/src/main/java/in/koreatech/koin/domain/repository/DeptRepository.kt index ea7107d33..9306ac919 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/repository/DeptRepository.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/repository/DeptRepository.kt @@ -5,4 +5,5 @@ import `in`.koreatech.koin.domain.model.user.Dept interface DeptRepository { suspend fun getDeptNameFromDeptCode(deptCode: String): String suspend fun getDepts(): List + suspend fun getDeptNames(): List } \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerRegisterRepository.kt b/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerRegisterRepository.kt index c34a8af56..342841e17 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerRegisterRepository.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerRegisterRepository.kt @@ -10,7 +10,7 @@ interface OwnerRegisterRepository { name: String, password: String, phoneNumber: String, - shopId: Int, + shopId: Int?, shopName: String ): Result } \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerSignupRepository.kt b/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerSignupRepository.kt index 8d3fd2018..2a13ad251 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerSignupRepository.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerSignupRepository.kt @@ -7,4 +7,8 @@ interface OwnerSignupRepository { suspend fun requestEmailVerification( email: String ): Result + + suspend fun requestSmsVerificationCode( + phoneNumber: String, + ): Result } \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerVerificationCodeRepository.kt b/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerVerificationCodeRepository.kt index dd7fa2951..81adb15c0 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerVerificationCodeRepository.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/repository/OwnerVerificationCodeRepository.kt @@ -8,4 +8,9 @@ interface OwnerVerificationCodeRepository { address: String, verificationCode: String ): Result + + suspend fun verifySmsCode( + phoneNumber: String, + verificationCode: String + ): Result } \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/repository/PreSignedUrlRepository.kt b/domain/src/main/java/in/koreatech/koin/domain/repository/PreSignedUrlRepository.kt index e53c63224..334d30931 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/repository/PreSignedUrlRepository.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/repository/PreSignedUrlRepository.kt @@ -10,4 +10,11 @@ interface PreSignedUrlRepository { mediaType: String, mediaSize: Long ): Result + + suspend fun uploadFile( + url: String, + bitmap: String, + mediaType: String, + mediaSize: Long + ): Result } \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/state/signup/SignupContinuationState.kt b/domain/src/main/java/in/koreatech/koin/domain/state/signup/SignupContinuationState.kt index 7d6e8e9d8..542d44b87 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/state/signup/SignupContinuationState.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/state/signup/SignupContinuationState.kt @@ -7,6 +7,7 @@ sealed class SignupContinuationState { object AvailableNickname: SignupContinuationState() // 이메일 중복 확인으로 사용 가능 object RequestedEmailValidation: SignupContinuationState() + object RequestedSmsValidation: SignupContinuationState() object CheckNickName: SignupContinuationState() // 닉네임 중복버튼 눌렀는지 확인 object CheckGender: SignupContinuationState() // 성별 라디오 버튼 눌렀는지 확인 object CheckGraduate: SignupContinuationState() // 졸업생 라디오 버튼을 눌렀는지 확인 @@ -15,6 +16,8 @@ sealed class SignupContinuationState { object InitPhoneNumber: SignupContinuationState() // 전화번호를 작성했는지 확인 object InitStudentId: SignupContinuationState() // 학번을 작성했는지 확인 + object SmsCodeIsNotValidate: SignupContinuationState() + object PhoneNumberIsNotValidate: SignupContinuationState() object EmailIsNotValidate: SignupContinuationState() object PasswordIsNotValidate: SignupContinuationState() object PasswordNotMatching: SignupContinuationState() diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/business/BusinessSignupCheckUseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/BusinessSignupCheckUseCase.kt new file mode 100644 index 000000000..7faa466b5 --- /dev/null +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/BusinessSignupCheckUseCase.kt @@ -0,0 +1,38 @@ +package `in`.koreatech.koin.domain.usecase.business + +import `in`.koreatech.koin.domain.repository.OwnerVerificationCodeRepository +import `in`.koreatech.koin.domain.repository.TokenRepository +import `in`.koreatech.koin.domain.state.signup.SignupContinuationState +import `in`.koreatech.koin.domain.util.ext.isNotValidPassword +import kotlinx.coroutines.CancellationException +import javax.inject.Inject + +class BusinessSignupCheckUseCase @Inject constructor( + private val ownerVerificationCodeRepository: OwnerVerificationCodeRepository, + private val tokenRepository: TokenRepository, +) { + suspend operator fun invoke( + password: String, + passwordCheck: String, + phoneNumber: String, + verificationCode: String + ): Result { + return when { + (password.isNotValidPassword()) -> Result.success(SignupContinuationState.PasswordIsNotValidate) + (password != passwordCheck) -> Result.success(SignupContinuationState.PasswordNotMatching) + (phoneNumber.length != 11) -> Result.success(SignupContinuationState.PhoneNumberIsNotValidate) + else -> { + val authToken = ownerVerificationCodeRepository.verifySmsCode( + phoneNumber, + verificationCode + ) + authToken.getOrDefault(defaultValue = null) + ?.let { tokenRepository.saveOwnerAccessToken(it.token) } + if (authToken.isFailure) { + Result.failure(authToken.exceptionOrNull() ?: CancellationException()) + } else + Result.success(SignupContinuationState.CheckComplete) + } + } + } +} diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/business/EmailAuthUseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/EmailAuthUseCase.kt new file mode 100644 index 000000000..97a01fee2 --- /dev/null +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/EmailAuthUseCase.kt @@ -0,0 +1,24 @@ +package `in`.koreatech.koin.domain.usecase.business + +import `in`.koreatech.koin.domain.repository.OwnerVerificationCodeRepository +import `in`.koreatech.koin.domain.repository.TokenRepository +import javax.inject.Inject + +class EmailAuthUseCase @Inject constructor( + private val ownerVerificationCodeRepository: OwnerVerificationCodeRepository, + private val tokenRepository: TokenRepository, +) { + suspend operator fun invoke( + address: String, + verificationCode: String + ): Result { + return try { + val authToken = ownerVerificationCodeRepository.compareVerificationCode(address, verificationCode) + authToken.getOrDefault(defaultValue = null) + ?.let { tokenRepository.saveOwnerAccessToken(it.token) } + Result.success(Unit) + } catch (t: Throwable) { + Result.failure(t) + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SearchStoresUsecase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SearchStoresUsecase.kt new file mode 100644 index 000000000..75dfd7516 --- /dev/null +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SearchStoresUsecase.kt @@ -0,0 +1,20 @@ +package `in`.koreatech.koin.domain.usecase.business + +import `in`.koreatech.koin.domain.model.store.Store +import `in`.koreatech.koin.domain.model.store.StoreCategory +import `in`.koreatech.koin.domain.repository.StoreRepository +import `in`.koreatech.koin.domain.util.ext.sortedOpenStore +import `in`.koreatech.koin.domain.util.match +import javax.inject.Inject + +class SearchStoresUseCase @Inject constructor( + private val storeRepository: StoreRepository, +) { + suspend operator fun invoke( + search: String? = null, + ): List { + return storeRepository.getStores() + .filter { if (search != null) it.name.match(search) else true } + .sortedOpenStore() + } +} diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SendSignupEmailUseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SendSignupEmailUseCase.kt deleted file mode 100644 index 597ea5174..000000000 --- a/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SendSignupEmailUseCase.kt +++ /dev/null @@ -1,33 +0,0 @@ -package `in`.koreatech.koin.domain.usecase.business - -import `in`.koreatech.koin.domain.repository.OwnerSignupRepository -import `in`.koreatech.koin.domain.state.signup.SignupContinuationState -import `in`.koreatech.koin.domain.util.ext.isNotBusinessValidEmail -import javax.inject.Inject - -class SendSignupEmailUseCase @Inject constructor( - private val ownerSignupRepository: OwnerSignupRepository -) { - operator fun invoke( - email: String, - password: String, - passwordConfirm: String, - ): Result { - return when { - password != passwordConfirm -> Result.success(SignupContinuationState.PasswordNotMatching) - email.trim().isNotBusinessValidEmail() -> Result.success(SignupContinuationState.EmailIsNotValidate) - else -> Result.success(SignupContinuationState.CheckComplete) - } - } - - suspend fun sendEmail( - email: String, - ): Result { - return ownerSignupRepository.requestEmailVerification( - email = email - ).map { SignupContinuationState.CheckComplete } - } - - - -} \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SendSignupSmsCodeUseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SendSignupSmsCodeUseCase.kt new file mode 100644 index 000000000..6f8866c9e --- /dev/null +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SendSignupSmsCodeUseCase.kt @@ -0,0 +1,20 @@ +package `in`.koreatech.koin.domain.usecase.business + +import `in`.koreatech.koin.domain.repository.OwnerSignupRepository +import `in`.koreatech.koin.domain.state.signup.SignupContinuationState +import javax.inject.Inject + +class SendSignupSmsCodeUseCase @Inject constructor( + private val ownerSignupRepository: OwnerSignupRepository +) { + suspend operator fun invoke( + phoneNumber: String, + ): Result { + return try { + ownerSignupRepository.requestSmsVerificationCode(phoneNumber) + Result.success(SignupContinuationState.RequestedSmsValidation) + } catch (t: Throwable) { + Result.failure(t) + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SmsVerificationUseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SmsVerificationUseCase.kt new file mode 100644 index 000000000..b5ffa3b01 --- /dev/null +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/SmsVerificationUseCase.kt @@ -0,0 +1,25 @@ +package `in`.koreatech.koin.domain.usecase.business + +import `in`.koreatech.koin.domain.repository.OwnerVerificationCodeRepository +import `in`.koreatech.koin.domain.repository.TokenRepository +import `in`.koreatech.koin.domain.state.signup.SignupContinuationState +import `in`.koreatech.koin.domain.util.ext.formatPhoneNumber +import javax.inject.Inject +class SmsVerificationUseCase @Inject constructor( + private val ownerVerificationCodeRepository: OwnerVerificationCodeRepository, + private val tokenRepository: TokenRepository, +) { + suspend operator fun invoke( + phoneNumber: String, + verificationCode: String + ): Result { + return try { + val authToken = ownerVerificationCodeRepository.verifySmsCode(phoneNumber.formatPhoneNumber(), verificationCode) + authToken.getOrDefault(defaultValue = null) + ?.let { tokenRepository.saveOwnerAccessToken(it.token) } + Result.success(SignupContinuationState.CheckComplete) + } catch (t: Exception) { + Result.failure(t) + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/business/UploadFileUseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/UploadFileUseCase.kt new file mode 100644 index 000000000..6c978dcd6 --- /dev/null +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/business/UploadFileUseCase.kt @@ -0,0 +1,17 @@ +package `in`.koreatech.koin.domain.usecase.business + +import `in`.koreatech.koin.domain.repository.PreSignedUrlRepository +import javax.inject.Inject + +class UploadFileUseCase @Inject constructor( + private val preSignedUrlRepository: PreSignedUrlRepository +) { + suspend operator fun invoke( + url: String, + bitmap: String, + mediaType: String, + mediaSize: Long + ): Result { + return preSignedUrlRepository.uploadFile(url, bitmap, mediaType, mediaSize) + } +} \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/dept/GetDeptNamesUseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/dept/GetDeptNamesUseCase.kt new file mode 100644 index 000000000..2414468ad --- /dev/null +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/dept/GetDeptNamesUseCase.kt @@ -0,0 +1,12 @@ +package `in`.koreatech.koin.domain.usecase.dept + +import `in`.koreatech.koin.domain.repository.DeptRepository +import javax.inject.Inject + +class GetDeptNamesUseCase @Inject constructor( + private val deptRepository: DeptRepository, +) { + suspend operator fun invoke(): List { + return deptRepository.getDeptNames() + } +} \ No newline at end of file diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/owner/OwnerRegisterUseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/owner/OwnerRegisterUseCase.kt index 330f523b8..3d0152348 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/usecase/owner/OwnerRegisterUseCase.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/owner/OwnerRegisterUseCase.kt @@ -14,7 +14,7 @@ class OwnerRegisterUseCase @Inject constructor( name: String, password: String, phoneNumber: String, - shopId: Int, + shopId: Int?, shopName: String ): Result { return ownerRegisterRepository.ownerRegister(attachments, companyNumber, email, name, password, phoneNumber, shopId, shopName) diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/signup/SignupRequestEmailVerificationUseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/signup/SignupRequestEmailVerificationUseCase.kt index 290a35b4b..e60017130 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/usecase/signup/SignupRequestEmailVerificationUseCase.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/signup/SignupRequestEmailVerificationUseCase.kt @@ -41,7 +41,7 @@ class SignupRequestEmailVerificationUseCase @Inject constructor( name = name, nickName = nickName, password = password, - phoneNumber = phoneNumber.formatPhoneNumber(), + phoneNumber = phoneNumber, studentNumber = studentNumber, ).map { SignupContinuationState.RequestedEmailValidation diff --git a/domain/src/main/java/in/koreatech/koin/domain/usecase/user/UpdateStudentUserInfoUseCase.kt b/domain/src/main/java/in/koreatech/koin/domain/usecase/user/UpdateStudentUserInfoUseCase.kt index 2d6dd232a..21d98f52c 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/usecase/user/UpdateStudentUserInfoUseCase.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/usecase/user/UpdateStudentUserInfoUseCase.kt @@ -7,13 +7,11 @@ import `in`.koreatech.koin.domain.error.user.UserErrorHandler import `in`.koreatech.koin.domain.model.error.ErrorHandler import `in`.koreatech.koin.domain.model.user.Gender import `in`.koreatech.koin.domain.model.user.User -import `in`.koreatech.koin.domain.repository.DeptRepository import `in`.koreatech.koin.domain.repository.UserRepository import `in`.koreatech.koin.domain.util.ext.isValidStudentId import javax.inject.Inject class UpdateStudentUserInfoUseCase @Inject constructor( - private val deptRepository: DeptRepository, private val userRepository: UserRepository, private val userErrorHandler: UserErrorHandler ) { @@ -24,6 +22,7 @@ class UpdateStudentUserInfoUseCase @Inject constructor( separatedPhoneNumber: List?, gender: Gender?, studentId: String, + major: String, checkedEmailValidation: Boolean ): ErrorHandler? { return try { @@ -35,15 +34,13 @@ class UpdateStudentUserInfoUseCase @Inject constructor( throw IllegalStateException(ERROR_USERINFO_GENDER_NOT_SET) } - if(!studentId.isValidStudentId) { + if (!studentId.isValidStudentId) { throw IllegalStateException(ERROR_INVALID_STUDENT_ID) } - val newUser: User = when(beforeUser) { + val newUser: User = when (beforeUser) { User.Anonymous -> throw IllegalAccessException() is User.Student -> { - val deptString = deptRepository.getDeptNameFromDeptCode(studentId.substring(5..6)) - beforeUser.copy( name = name.trim(), nickname = nickname.trim(), @@ -55,7 +52,7 @@ class UpdateStudentUserInfoUseCase @Inject constructor( }, gender = gender, studentNumber = studentId.trim().ifBlank { null }, - major = deptString + major = major ) } } diff --git a/domain/src/main/java/in/koreatech/koin/domain/util/HangulInitialSoundSearch.kt b/domain/src/main/java/in/koreatech/koin/domain/util/HangulInitialSoundSearch.kt index fc30a23c4..571999d39 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/util/HangulInitialSoundSearch.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/util/HangulInitialSoundSearch.kt @@ -55,6 +55,7 @@ object HangulInitialSoundSearch { val slen = trimSearch.length if (seof < 0) return false + if (trimValue.contains(trimSearch)) return true for (i in 0..seof) { t = 0 diff --git a/domain/src/main/java/in/koreatech/koin/domain/util/ext/StringExtensions.kt b/domain/src/main/java/in/koreatech/koin/domain/util/ext/StringExtensions.kt index 1bc0aaa99..6e26cc07f 100644 --- a/domain/src/main/java/in/koreatech/koin/domain/util/ext/StringExtensions.kt +++ b/domain/src/main/java/in/koreatech/koin/domain/util/ext/StringExtensions.kt @@ -19,3 +19,7 @@ fun String.toColorForHtml(color: String) = "