diff --git a/build.gradle.kts b/build.gradle.kts index 7bfbd1805..ade36d71e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,6 +9,11 @@ buildscript { extra.apply { set("versionName", "4.0.4") set("versionCode", 40004) + // 코인 버전 관리 + + set("versionBusinessName", "1.0.0") + set("versionBusinessCode", 10000) + //코안 사장님 버전 관리 } dependencies { diff --git a/business/build.gradle.kts b/business/build.gradle.kts index 5291f26b2..262215f2c 100644 --- a/business/build.gradle.kts +++ b/business/build.gradle.kts @@ -11,6 +11,12 @@ plugins { android { namespace = "in.koreatech.business" + defaultConfig { + applicationId = "in.koreatech.business" + versionCode = rootProject.extra["versionBusinessCode"] as Int + versionName = rootProject.extra["versionBusinessName"].toString() + } + androidComponents { onVariants(selector().withBuildType("release")) { it.packaging.resources.excludes.add("META-INF/**") @@ -41,6 +47,7 @@ dependencies { implementation(libs.androidx.security.crypto) implementation(libs.coil) implementation(libs.coil.compose) + implementation(libs.coil.gif) implementation(libs.compose.numberPicker) implementation(libs.androidx.security.crypto) implementation(libs.compose.numberPicker) diff --git a/business/src/main/AndroidManifest.xml b/business/src/main/AndroidManifest.xml index 9d17bae9d..31ffcde68 100644 --- a/business/src/main/AndroidManifest.xml +++ b/business/src/main/AndroidManifest.xml @@ -5,7 +5,9 @@ diff --git a/business/src/main/java/in/koreatech/business/feature/ImagePickerHandler.kt b/business/src/main/java/in/koreatech/business/feature/ImagePickerHandler.kt new file mode 100644 index 000000000..de49fee78 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/ImagePickerHandler.kt @@ -0,0 +1,66 @@ +package `in`.koreatech.business.feature + +import android.content.ContentResolver +import android.net.Uri +import android.provider.OpenableColumns +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +@Composable +fun launchImagePicker( + contentResolver: ContentResolver, + maxItem: Int = 3, + initImageUrls: () -> Unit, + getPreSignedUrl: (Pair, Pair>) -> Unit = {}, + clearFileInfo: () -> Unit = {}, +): ManagedActivityResultLauncher> { + val coroutineScope = rememberCoroutineScope() + val galleryLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickMultipleVisualMedia(maxItem) + ) { uriList -> + clearFileInfo() + initImageUrls() + uriList.forEach { + coroutineScope.launch { + var fileName = "" + var fileSize = 0L + val inputStream = contentResolver.openInputStream(it) + withContext(Dispatchers.IO) { + if (it.scheme.equals("content")) { + val cursor = 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) + } + } + } + if (inputStream != null) { + getPreSignedUrl( + Pair( + Pair(fileSize, "image/" + fileName.split(".")[1]), + Pair(fileName, it.toString()) + ) + ) + } + inputStream?.close() + } + } + } + } + } + return galleryLauncher +} diff --git a/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventScreen.kt b/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventScreen.kt new file mode 100644 index 000000000..c1ccd0661 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventScreen.kt @@ -0,0 +1,579 @@ +package `in`.koreatech.business.feature.event.writeevent.writeevent + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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 +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +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.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.layout.ContentScale +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.text.input.KeyboardType +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 `in`.koreatech.business.ui.theme.Black1 +import `in`.koreatech.business.ui.theme.ColorMinor +import `in`.koreatech.business.ui.theme.ColorPrimary +import `in`.koreatech.business.ui.theme.ColorSecondary +import `in`.koreatech.business.ui.theme.ColorTextDescription +import `in`.koreatech.business.ui.theme.ColorTextField +import `in`.koreatech.business.ui.theme.Gray1 +import `in`.koreatech.business.ui.theme.Gray6 +import `in`.koreatech.koin.core.R +import org.orbitmvi.orbit.compose.collectAsState + +@Composable +fun WriteEventScreen( + goToMyStoreScreen: () -> Unit = {}, + onBackPressed: () -> Unit = {}, + viewModel: WriteEventViewModel = hiltViewModel() +) { + val state = viewModel.collectAsState().value + WriteEventScreenImpl( + onBackPressed = onBackPressed, + writeEventState = state, + onChangeTitle = viewModel::onTitleChanged, + onChangeContent = viewModel::onContentChanged, + onRegisterImage = viewModel::registerEventImageUri, + onDeleteImage = viewModel::deleteImage, + onStartYearChanged = viewModel::onStartYearChanged, + onStartMonthChanged = viewModel::onStartMonthChanged, + onStartDayChanged = viewModel::onStartDayChanged, + onEndYearChanged = viewModel::onEndYearChanged, + onEndMonthChanged = viewModel::onEndMonthChanged, + onEndDayChanged = viewModel::onEndDayChanged, + ) +} + +@Composable +fun WriteEventScreenImpl( + onBackPressed: () -> Unit = {}, + writeEventState: WriteEventState = WriteEventState(), + onChangeTitle: (String) -> Unit = {}, + onChangeContent: (String) -> Unit = {}, + onRegisterImage: (Uri) -> Unit = {}, + onDeleteImage: (Int) -> Unit = {}, + onStartYearChanged:(String) -> Unit = {}, + onStartMonthChanged:(String) -> Unit = {}, + onStartDayChanged:(String) -> Unit = {}, + onEndYearChanged:(String) -> Unit = {}, + onEndMonthChanged:(String) -> Unit = {}, + onEndDayChanged:(String) -> Unit = {}, + onNextButtonClicked: () -> Unit = {} +) { + val singlePhotoPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri -> + if (uri != null) { + onRegisterImage(uri) + } + } + ) + + Column( + modifier = Modifier.fillMaxSize() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(62.dp) + .background(ColorPrimary), + ) { + IconButton( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 16.dp), + onClick = { onBackPressed() } + ) { + Image( + painter = painterResource(id = R.drawable.ic_white_arrow_back), + contentDescription = stringResource(R.string.back), + ) + } + Text( + text = stringResource(R.string.event_write), + modifier = Modifier.align(Alignment.Center), + style = TextStyle(color = Color.White, fontSize = 20.sp), + ) + } + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + item { + Text( + modifier = Modifier.padding(start = 24.dp, top = 24.dp), + text = stringResource(id = R.string.menu_name), + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + Row( + modifier = Modifier + .fillMaxWidth() + ) + { + Text( + modifier = Modifier.padding(start = 24.dp), + text = stringResource(id = R.string.event_upload_image_instruction), + fontSize = 12.sp, + color = Gray6 + ) + + Spacer(modifier = Modifier.weight(1f)) + + CountLimitText( + text = "${writeEventState.images.size}/${WriteEventViewModel.MAX_IMAGE_LENGTH}", + inputTextLength = writeEventState.images.size, + limit = WriteEventViewModel.MAX_IMAGE_LENGTH + ) + } + + if(writeEventState.images.isNotEmpty()){ + EventImageView( + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(top = 5.dp) + .fillMaxWidth() + .height(112.dp) + , + imageList = writeEventState.images, + onDeleteImage = onDeleteImage + ) + } + Row( + modifier = Modifier + .padding(top = 5.dp) + .padding(horizontal = 24.dp) + .fillMaxWidth() + .clickable( + enabled = writeEventState.images.size < WriteEventViewModel.MAX_IMAGE_LENGTH + ) { + singlePhotoPickerLauncher.launch( + PickVisualMediaRequest( + ActivityResultContracts.PickVisualMedia.ImageOnly + ) + ) + } + .clip(RoundedCornerShape(5.dp)) + .background(ColorTextField), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(R.drawable.ic_add_image), + contentDescription = stringResource(id = R.string.register_image), + modifier = Modifier.size(20.dp), + colorFilter = if (writeEventState.images.size == WriteEventViewModel.MAX_IMAGE_LENGTH) ColorFilter.tint( + Gray6 + ) else null + ) + Text( + text = stringResource(id = R.string.register_image), + modifier = Modifier.padding(start = 8.dp, top = 12.dp, bottom = 12.dp), + fontWeight = FontWeight.Bold, + color = if (writeEventState.images.size == WriteEventViewModel.MAX_IMAGE_LENGTH) Gray6 else Gray1 + ) + } + } + + item { + Row( + modifier = Modifier + .padding(start = 24.dp, top = 22.dp) + .fillMaxWidth() + ) + { + Text( + text = stringResource(id = R.string.title), + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.weight(1f)) + + CountLimitText( + text = "${writeEventState.title.length}/${WriteEventViewModel.MAX_TITLE_LENGTH}", + inputTextLength = writeEventState.title.length, + limit = WriteEventViewModel.MAX_TITLE_LENGTH + ) + } + + BorderTextField( + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(top = 5.dp), + textFieldModifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 12.dp), + inputString = writeEventState.title, + onStringChange = onChangeTitle, + hintString = stringResource(id = R.string.event_input_title_instruction) + ) + + } + + item { + Row( + modifier = Modifier + .padding(start = 24.dp, top = 22.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(186.dp) + ) + { + Text( + text = stringResource(id = R.string.event_content), + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.weight(1f)) + + CountLimitText( + text = "${writeEventState.content.length}/${WriteEventViewModel.MAX_CONTENT_LENGTH}", + inputTextLength = writeEventState.content.length, + limit = WriteEventViewModel.MAX_CONTENT_LENGTH + ) + } + BorderTextField( + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(top = 5.dp) + .height(123.dp), + textFieldModifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 12.dp), + inputString = writeEventState.content, + onStringChange = onChangeContent, + hintString = stringResource(id = R.string.event_input_content_instruction) + ) + } + + item { + Text( + modifier = Modifier.padding(start = 24.dp, top = 22.dp), + text = stringResource(id = R.string.event_period), + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + + DateInputRow( + modifier = Modifier + .height(40.dp) + .width(60.dp), + optionString = stringResource(id = R.string.start_date), + year = writeEventState.startYear, + month = writeEventState.startMonth, + day= writeEventState.startDay, + onYearChanged = onStartYearChanged, + onMonthChanged= onStartMonthChanged, + onDayChanged= onStartDayChanged + ) + + DateInputRow( + modifier = Modifier + .height(40.dp) + .width(60.dp), + optionString = stringResource(id = R.string.end_date), + year = writeEventState.endYear, + month = writeEventState.endMonth, + day= writeEventState.endDay, + onYearChanged = onEndYearChanged, + onMonthChanged= onEndMonthChanged, + onDayChanged= onEndDayChanged + ) + } + + item { + Row( + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(top = 200.dp, bottom = 20.dp) + .fillMaxWidth() + .height(43.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Button( + onClick = {onBackPressed()}, + shape = RectangleShape, + colors = ButtonDefaults.buttonColors(ColorTextField), + modifier = Modifier + .fillMaxHeight() + .width(120.dp) + ) { + Text( + text = stringResource(id = R.string.common_do_cancellation), + fontSize = 15.sp, + color = Gray1 + ) + + Image( + painter = painterResource(id = R.drawable.ic_cancel), + contentDescription = stringResource(id = R.string.common_do_cancellation) + ) + } + + Button( + onClick = onNextButtonClicked, + shape = RectangleShape, + colors = ButtonDefaults.buttonColors(ColorPrimary), + modifier = Modifier + .fillMaxSize() + ) { + Text( + text = stringResource(id = R.string.register), + fontSize = 15.sp, + color = Color.White + ) + Image( + painter = painterResource(id = R.drawable.ic_register), + contentDescription = stringResource(id = R.string.register) + ) + } + } + } + } + } +} + +@Composable +fun BorderTextField( + modifier: Modifier = Modifier, + textFieldModifier: Modifier = Modifier, + inputString: String = "", + onStringChange: (String) -> Unit = {}, + hintString: String = "", + textAlign: TextAlign = TextAlign.Unspecified, + contentAlignment : Alignment = Alignment.TopStart, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default +) { + Box( + modifier = modifier + .border(width = 1.dp, color = ColorMinor, shape = RoundedCornerShape(4.dp)) + , + contentAlignment = Alignment.CenterStart + ) { + BasicTextField( + value = inputString, + onValueChange = onStringChange, + textStyle = TextStyle( + color = Color.Black, + fontSize = 14.sp, + textAlign = textAlign + ), + keyboardOptions = keyboardOptions, + modifier = textFieldModifier, + decorationBox = { innerTextField -> + Box( + modifier = Modifier + .fillMaxWidth(), + contentAlignment = contentAlignment + ) { + if (inputString.isEmpty()) { + Text( + text = hintString, + style = TextStyle( + color = Color.Gray, + fontSize = 14.sp, + textAlign = textAlign + ) + ) + } + innerTextField() + } + } + ) + } +} + +@Composable +fun EventImageView( + modifier: Modifier = Modifier, + imageList: List = emptyList(), + onDeleteImage: (Int) -> Unit = {} +) { + Box( + modifier = modifier + .fillMaxWidth() + .background(ColorTextField), + ) { + LazyRow( + modifier = Modifier + .padding(horizontal = 10.dp) + .padding(top = 8.dp) + ) { + itemsIndexed(imageList) { index, item -> + Box( + modifier = Modifier + .size(110.dp) + .padding(end = 10.dp) + .padding(bottom = 8.dp) + , + contentAlignment = Alignment.TopEnd + ) + { + Image( + modifier = Modifier + .size(96.dp), + painter = rememberAsyncImagePainter( + item + ), + contentDescription = "", + contentScale = ContentScale.Crop + ) + Image( + modifier = Modifier + .size(16.dp) + .offset(x = 7.dp, y = (-7).dp) + .clickable { + onDeleteImage(index) + }, + painter = painterResource(id = R.drawable.ic_delete_button), + contentDescription = "" + ) + } + } + } + } +} + +@Composable +private fun CountLimitText( + text: String, + inputTextLength: Int, + limit: Int +) { + Text( + color = if(inputTextLength == limit) ColorSecondary else ColorTextDescription, + modifier = Modifier.padding(end = 24.dp), + fontSize = 12.sp, + text = text + ) +} + +@Composable +private fun DateInputRow( + modifier: Modifier = Modifier, + optionString: String = "시작일", + year: String = "", + month: String = "", + day: String = "", + onYearChanged: (String) -> Unit = {}, + onMonthChanged: (String) -> Unit = {}, + onDayChanged: (String) -> Unit = {}, +) { + Row( + modifier = Modifier + .padding(top = 11.dp) + .padding(horizontal = 24.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically + ) { + Box(modifier = Modifier + .clip(RoundedCornerShape(999.dp)) + .background(ColorTextField) + ) { + Text( + text = optionString, + fontWeight = FontWeight.Bold, + color = Black1, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + + Spacer(modifier = Modifier.size(12.5.dp)) + + BorderTextField( + modifier = modifier, + inputString= year, + onStringChange= onYearChanged, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + hintString = "2999", + contentAlignment = Alignment.Center, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.size(12.5.dp)) + + Text(text = "/", fontWeight = FontWeight.Bold) + + Spacer(modifier = Modifier.size(12.5.dp)) + + BorderTextField( + modifier = modifier + , + inputString= month, + onStringChange= onMonthChanged, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + hintString = "01", + contentAlignment = Alignment.Center, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.size(12.5.dp)) + + Text(text = "/", fontWeight = FontWeight.Bold) + + Spacer(modifier = Modifier.size(12.5.dp)) + + BorderTextField( + modifier = modifier, + inputString= day, + onStringChange= onDayChanged, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + hintString = "01", + contentAlignment = Alignment.Center, + textAlign = TextAlign.Center + ) + } +} + +@Preview +@Composable +fun PreviewWriteEventScreen() { + WriteEventScreenImpl() +} + +@Preview +@Composable +fun PreviewImageview() { + DateInputRow() +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventSideEffect.kt new file mode 100644 index 000000000..faf2a70ef --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventSideEffect.kt @@ -0,0 +1,5 @@ +package `in`.koreatech.business.feature.event.writeevent.writeevent + +sealed class WriteEventSideEffect { + data object ToastImageLimit : WriteEventSideEffect() +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventState.kt b/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventState.kt new file mode 100644 index 000000000..4c356a96b --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventState.kt @@ -0,0 +1,18 @@ +package `in`.koreatech.business.feature.event.writeevent.writeevent + +import android.net.Uri + +data class WriteEventState( + val title: String = "", + val content: String = "", + val startYear: String = "", + val startMonth: String = "", + val startDay: String = "", + val endYear: String = "", + val endMonth: String = "", + val endDay: String = "", + val images: List = emptyList(), + val showTitleInputAlert: Boolean = false, + val showContentInputAlert: Boolean = false, + val showDateInputAlert: Boolean = false, +) \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventViewModel.kt b/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventViewModel.kt new file mode 100644 index 000000000..57d41631f --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/event/writeevent/writeevent/WriteEventViewModel.kt @@ -0,0 +1,166 @@ +package `in`.koreatech.business.feature.event.writeevent.writeevent + +import android.net.Uri +import androidx.lifecycle.ViewModel +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.blockingIntent +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 + +class WriteEventViewModel( + +) : ViewModel(), ContainerHost { + override val container = container(WriteEventState()) + + fun onTitleChanged(title: String) = blockingIntent { + if(title.length > MAX_TITLE_LENGTH) + return@blockingIntent + reduce { + state.copy(title = title, showTitleInputAlert = false) + } + } + + fun onContentChanged(content: String) = blockingIntent { + if(content.length > MAX_CONTENT_LENGTH) + return@blockingIntent + reduce { + state.copy(content = content, showContentInputAlert = false) + } + } + + fun onStartYearChanged(startYear: String) = intent { + if(isValidNumberInput(4, startYear).not()) + return@intent + reduce { + state.copy(startYear = startYear, + showDateInputAlert = if (state.showDateInputAlert) + !isAllDateInputFilled(state.copy(startYear = startYear)) + else false + ) + } + } + + fun onStartMonthChanged(startMonth: String) = intent { + if(isValidNumberInput(2, startMonth).not()) + return@intent + reduce { + state.copy(startMonth = startMonth, + showDateInputAlert = if (state.showDateInputAlert) + !isAllDateInputFilled(state.copy(startMonth = startMonth)) + else false + ) + } + } + + fun onStartDayChanged(startDay: String) = intent { + if(isValidNumberInput(2, startDay).not()) + return@intent + reduce { + state.copy(startDay = startDay, + showDateInputAlert = if (state.showDateInputAlert) + !isAllDateInputFilled(state.copy(startDay = startDay)) + else false + ) + } + } + + fun onEndYearChanged(endYear: String) = intent { + if(isValidNumberInput(4, endYear).not()) + return@intent + reduce { + state.copy(endYear = endYear, + showDateInputAlert = if (state.showDateInputAlert) + !isAllDateInputFilled(state.copy(endYear = endYear)) + else false + ) + } + } + + fun onEndMonthChanged(endMonth: String) = intent { + if(isValidNumberInput(2, endMonth).not()) + return@intent + reduce { + state.copy(endMonth = endMonth, + showDateInputAlert = if (state.showDateInputAlert) + !isAllDateInputFilled(state.copy(endMonth = endMonth)) + else false + ) + } + } + + fun onEndDayChanged(endDay: String) = intent { + if(isValidNumberInput(2, endDay).not()) + return@intent + reduce { + state.copy(endDay = endDay, + showDateInputAlert = if (state.showDateInputAlert) + !isAllDateInputFilled(state.copy(endDay = endDay)) + else false + ) + } + } + + fun registerEventImageUri(imageUri: Uri) { + intent { + val newMenuUriList = state.images.toMutableList() + newMenuUriList.add(imageUri) + reduce { + state.copy( + images = newMenuUriList + ) + } + } + } + + fun deleteImage(index: Int) { + intent { + val newMenuUriList = state.images.toMutableList() + newMenuUriList.removeAt(index) + reduce { + state.copy( + images = newMenuUriList + ) + } + } + } + + fun registerEvent() = intent { + reduce { + state.copy( + showTitleInputAlert = state.title.isEmpty(), + showContentInputAlert = state.content.isEmpty(), + showDateInputAlert = state.startYear.length != 4 + || state.startMonth.length != 2 + || state.startDay.length != 2 + || state.endYear.length != 4 + || state.endMonth.length != 2 + || state.endDay.length != 2 + ) + } + } + + private fun isAllDateInputFilled(state: WriteEventState) : Boolean { + return state.startYear.length == 4 + && state.startMonth.length == 2 + && state.startDay.length == 2 + && state.endYear.length == 4 + && state.endMonth.length == 2 + && state.endDay.length == 2 + } + + private fun isValidNumberInput(maxLength: Int, input: String): Boolean { + if(input.length > maxLength) + return false + if(input.isNotEmpty() && input.toIntOrNull() == null) + return false + return true + } + + companion object { + const val MAX_IMAGE_LENGTH = 3 + const val MAX_TITLE_LENGTH = 25 + const val MAX_CONTENT_LENGTH = 500 + } +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/forcrupdate/ForceUpdateDialog.kt b/business/src/main/java/in/koreatech/business/feature/forcrupdate/ForceUpdateDialog.kt new file mode 100644 index 000000000..8f755d40c --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/forcrupdate/ForceUpdateDialog.kt @@ -0,0 +1,124 @@ +package `in`.koreatech.business.feature.forcrupdate + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.OutlinedButton +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +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 androidx.compose.ui.window.Dialog +import `in`.koreatech.business.ui.theme.ColorPrimary +import `in`.koreatech.business.ui.theme.Gray1 +import `in`.koreatech.business.ui.theme.Gray500 +import `in`.koreatech.koin.core.R + +@Composable +fun ForceUpdateDialog( + isShow :MutableState = remember { mutableStateOf(true) } +) { + if(isShow.value){ + Dialog( + onDismissRequest = { isShow.value = false } + ) { + Surface( + shape = RoundedCornerShape(4.dp), + color = Color.White, + elevation = 8.dp + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(id = R.string.force_update_already_update), + fontSize = 18.sp, + color = Color.Black, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp) + ) + + Text( + text = stringResource(id = R.string.force_update_dialog_content), + fontSize = 13.sp, + color = Gray1, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 32.dp) + .padding(top = 8.dp) + .fillMaxWidth() + + ) + + Row( + modifier = Modifier + .padding(horizontal = 32.dp) + .padding(top = 16.dp, bottom = 24.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ){ + OutlinedButton( + onClick = { isShow.value = false }, + shape = RoundedCornerShape(4.dp), + border = BorderStroke(1.dp, Gray500), + colors = ButtonDefaults.buttonColors(Color.White), + modifier = Modifier + .weight(1f) + .wrapContentHeight() + ) { + Text( + text = stringResource(id = R.string.positive), + fontSize = 14.sp, + color = Gray500 + ) + } + + Button( + onClick = { + isShow.value = false + //Todo: 플레이스토어 링크 넣기 + }, + shape = RoundedCornerShape(4.dp), + colors = ButtonDefaults.buttonColors(ColorPrimary), + modifier = Modifier + .weight(1f) + .wrapContentHeight() + ) { + Text( + text = stringResource(id = R.string.force_update_go_store), + fontSize = 14.sp, + color = Color.White + ) + } + } + } + } + } + } +} + +@Preview +@Composable +fun PreviewForceUpdateDialog(){ + ForceUpdateDialog() +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/forcrupdate/ForceUpdateScreen.kt b/business/src/main/java/in/koreatech/business/feature/forcrupdate/ForceUpdateScreen.kt new file mode 100644 index 000000000..6eacc43df --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/forcrupdate/ForceUpdateScreen.kt @@ -0,0 +1,170 @@ +package `in`.koreatech.business.feature.forcrupdate + +import android.app.Activity +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.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.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.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.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.size.Dimension +import coil.size.Size +import `in`.koreatech.business.ui.theme.ColorPrimary600 +import `in`.koreatech.business.ui.theme.ColorSecondary +import `in`.koreatech.business.util.GifImage +import `in`.koreatech.business.util.getDrawableResSize +import `in`.koreatech.koin.core.R + +@Composable +fun ForceUpdateScreen( + title: String ="", + content: String = "" +) { + val openDialog = remember { mutableStateOf(false) } + val context = LocalContext.current + val drawableResSize: Pair = getDrawableResSize(context, R.drawable.koin_logo_gif) + + Column( + modifier = Modifier + .fillMaxSize() + .background(ColorPrimary600) + ) { + Box( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .padding(top = 18.dp) + .fillMaxWidth() + ){ + Spacer(modifier = Modifier.weight(1f)) + + Box( + modifier = Modifier + .padding(end = 29.dp) + .size(24.dp) + .clickable { (context as? Activity)?.finish() } + + ) { + Image( + painter = painterResource(R.drawable.ic_exit), + contentDescription = "backArrow", + modifier = Modifier.fillMaxSize() + ) + } + } + GifImage( + modifier = Modifier + .padding(top = 24.dp) + .height(250.dp) + .fillMaxWidth() + , + painterResource = R.drawable.koin_logo_gif, + imageSize = Size(drawableResSize.first, drawableResSize.second) + ) + } + + Text( + text = title, + fontSize = 20.sp, + color = Color.White, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 46.dp) + ) + + ForceUpdateDialog( + isShow = openDialog + ) + + Text( + text = content, + fontSize = 14.sp, + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 48.dp) + ) + + Button( + onClick = { + //Todo: 플레이스토어 링크 넣기 + }, + colors = ButtonDefaults.buttonColors(ColorSecondary), + shape = RoundedCornerShape(4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 100.dp) + .padding(top = 72.dp) + .height(48.dp) + ) { + Text( + text = stringResource(id = R.string.force_update_do_update), + fontSize = 18.sp, + color = Color.White + ) + } + + Text( + text = stringResource(id = R.string.force_update_already_update), + fontSize = 12.sp, + color = Color.White, + textAlign = TextAlign.Center, + textDecoration = TextDecoration.Underline, + modifier = Modifier + .fillMaxWidth() + .padding(top = 48.dp) + .clickable{ + openDialog.value = true + } + ) + + Text( + text = stringResource(id = R.string.copyright_2024), + fontSize = 12.sp, + color = Color.White, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 103.dp) + ) + } +} + +@Preview +@Composable +fun PreviewForceUpdateScreen() { + ForceUpdateScreen( + + ) +} \ 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 index 2c80dd958..7a73638c5 100644 --- 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 @@ -3,6 +3,7 @@ package `in`.koreatech.business.feature.insertstore.insertdetailinfo import android.net.Uri import android.os.Parcelable import `in`.koreatech.business.feature.insertstore.insertdetailinfo.operatingTime.OperatingTimeState +import `in`.koreatech.koin.domain.model.owner.SettingTime import kotlinx.parcelize.Parcelize @Parcelize @@ -28,6 +29,6 @@ data class InsertDetailInfoScreenState ( ), val isDetailInfoValid: Boolean = false, val showDialog: Boolean = false, - val isOpenTimeSetting:Boolean = false, + val isOpenTimeSetting: SettingTime = SettingTime.OPEN, val dayOfWeekIndex: Int = -1 ): 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 index 05e6cba58..2eb03bf2e 100644 --- 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 @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import com.chargemap.compose.numberpicker.Hours import dagger.hilt.android.lifecycle.HiltViewModel import `in`.koreatech.business.feature.insertstore.insertmaininfo.InsertBasicInfoScreenState +import `in`.koreatech.koin.domain.model.owner.SettingTime import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.blockingIntent @@ -49,9 +50,9 @@ class InsertDetailInfoScreenViewModel @Inject constructor( } } - private fun isOpenTimeSetting(isOpenTimeSetting: Boolean) = intent{ + private fun isOpenTimeSetting(openTimeSetting: SettingTime) = intent{ reduce{ - state.copy(isOpenTimeSetting = isOpenTimeSetting) + state.copy(isOpenTimeSetting = openTimeSetting) } } @@ -104,7 +105,7 @@ class InsertDetailInfoScreenViewModel @Inject constructor( reduce { state.copy(showDialog = true) } - isOpenTimeSetting(true) + isOpenTimeSetting(SettingTime.OPEN) dayOfIndex(index) } @@ -112,7 +113,7 @@ class InsertDetailInfoScreenViewModel @Inject constructor( reduce { state.copy(showDialog = true) } - isOpenTimeSetting(false) + isOpenTimeSetting(SettingTime.CLOSE) dayOfIndex(index) } diff --git a/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/operatingTime/OperatingTimeSettingScreen.kt b/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/operatingTime/OperatingTimeSettingScreen.kt index 809deae72..a3dfa2f15 100644 --- a/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/operatingTime/OperatingTimeSettingScreen.kt +++ b/business/src/main/java/in/koreatech/business/feature/insertstore/insertdetailinfo/operatingTime/OperatingTimeSettingScreen.kt @@ -2,11 +2,14 @@ package `in`.koreatech.business.feature.insertstore.insertdetailinfo.operatingTi import androidx.compose.foundation.Image import androidx.compose.foundation.background +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 +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -16,10 +19,12 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -37,16 +42,19 @@ 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.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import com.chargemap.compose.numberpicker.FullHours import com.chargemap.compose.numberpicker.Hours import com.chargemap.compose.numberpicker.HoursNumberPicker import `in`.koreatech.business.feature.insertstore.insertdetailinfo.InsertDetailInfoScreenViewModel import `in`.koreatech.business.feature.insertstore.insertdetailinfo.dialog.OperatingTimeDialog +import `in`.koreatech.business.ui.theme.ColorMinor import `in`.koreatech.business.ui.theme.ColorPrimary import `in`.koreatech.business.ui.theme.ColorSecondaryText import `in`.koreatech.business.ui.theme.ColorTextBackgrond import `in`.koreatech.koin.core.R +import `in`.koreatech.koin.domain.model.owner.SettingTime import org.orbitmvi.orbit.compose.collectAsState @Composable @@ -88,7 +96,7 @@ fun OperatingTimeSettingScreen( @Composable fun OperatingTimeSettingScreenImpl( showDialog: Boolean = false, - isOpenTimeSetting: Boolean = false, + isOpenTimeSetting: SettingTime = SettingTime.OPEN, dayOfWeekIndex: Int = 0, onShowOpenTimeDialog: (Int) -> Unit = {}, onShowCloseTimeDialog: (Int) -> Unit = {}, @@ -176,11 +184,11 @@ fun OperatingTimeSettingScreenImpl( ) { itemsIndexed(operatingTimeList) { index, item -> DayOperatingTimeSetting(item, onShowOpenTimeDialog, onShowCloseTimeDialog, index, onCheckBoxClicked) - } } when(isOpenTimeSetting){ - true -> ShowOpenTimeDialog( + SettingTime.OPEN -> OperatingTimeSettingDialog( + title = stringResource(id = R.string.store_open_time), operatingTimeDialog = OperatingTimeDialog( showDialog, onCloseDialog, @@ -188,7 +196,8 @@ fun OperatingTimeSettingScreenImpl( onSettingStoreOpenTime ) ) - false -> ShowCloseTimeDialog( + SettingTime.CLOSE -> OperatingTimeSettingDialog( + title = stringResource(id = R.string.store_close_time), operatingTimeDialog = OperatingTimeDialog( showDialog, onCloseDialog, @@ -247,6 +256,7 @@ fun DayOperatingTimeSetting( if(!operatingTime.closed) onShowOpenTimeDialog(index) }, text = operatingTime.openTime, + color =if (operatingTime.closed) ColorMinor else Color.Black, fontSize = 15.sp ) @@ -261,6 +271,7 @@ fun DayOperatingTimeSetting( if(!operatingTime.closed) onShowCloseTimeDialog(index) }, text = operatingTime.closeTime, + color =if (operatingTime.closed) ColorMinor else Color.Black, fontSize = 15.sp ) @@ -277,127 +288,76 @@ fun DayOperatingTimeSetting( } @Composable -fun ShowOpenTimeDialog( +fun OperatingTimeSettingDialog( + title: String = "", operatingTimeDialog: OperatingTimeDialog = OperatingTimeDialog() ) { - var openTimeValue by remember { mutableStateOf(FullHours(0, 0)) } + var timeValue by remember { mutableStateOf(FullHours(0, 0)) } if (operatingTimeDialog.showDialog) { - AlertDialog( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - , - onDismissRequest = { operatingTimeDialog.closeDialog() }, - title = { - Text( - modifier = Modifier.padding(bottom = 30.dp), - text = stringResource(id = R.string.store_open_time) - ) - }, - text = { - HoursNumberPicker( + Dialog(onDismissRequest = { operatingTimeDialog.closeDialog() }) { + Surface( + shape = RoundedCornerShape(16.dp), + color = Color.White, + elevation = 8.dp + ) { + Column( modifier = Modifier + .padding(16.dp) .fillMaxWidth() - .wrapContentHeight(), - dividersColor = MaterialTheme.colors.primary, - leadingZero = false, - value = openTimeValue, - onValueChange = { - openTimeValue = it - }, - minutesRange = (0..59 step 5), - hoursDivider = { - Text( - modifier = Modifier.size(24.dp), - textAlign = TextAlign.Center, - text = ":" - ) - } - ) - }, - confirmButton = { - Button( - onClick = { - operatingTimeDialog.onSettingStoreTime(Pair(openTimeValue, operatingTimeDialog.dayOfWeekIndex)) - }) { - Text(stringResource(id = R.string.positive)) - } - }, - dismissButton = { - Button( - onClick = { - operatingTimeDialog.closeDialog() - }) { - Text(stringResource(id = R.string.cancel)) - } - } - ) - } -} + ) { + Text( + modifier = Modifier.padding(bottom = 8.dp), + text = title, + style = MaterialTheme.typography.h6, + fontSize = 20.sp + ) -@Composable -fun ShowCloseTimeDialog( - operatingTimeDialog: OperatingTimeDialog = OperatingTimeDialog() -) { - var closeTimeValue by remember { mutableStateOf(FullHours(0, 0)) } - if (operatingTimeDialog.showDialog) { - AlertDialog( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - , - onDismissRequest = { operatingTimeDialog.closeDialog() }, - title = { - Text( - modifier = Modifier.padding(bottom = 30.dp), - text = stringResource(id = R.string.store_close_time) - ) - }, - text = { - HoursNumberPicker( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - dividersColor = MaterialTheme.colors.primary, - leadingZero = false, - value = closeTimeValue, - onValueChange = { - closeTimeValue = it - }, - minutesRange = (0..59 step 5), - hoursDivider = { - Text( - modifier = Modifier.size(24.dp), - textAlign = TextAlign.Center, - text = ":" - ) + HoursNumberPicker( + modifier = Modifier + .height(120.dp), + dividersColor = MaterialTheme.colors.primary, + leadingZero = true, + value = timeValue, + onValueChange = { + timeValue = it + }, + minutesRange = (0..59 step 5), + hoursDivider = { + Text( + modifier = Modifier.size(24.dp), + textAlign = TextAlign.Center, + text = ":" + ) + } + ) + Row( + modifier = Modifier + .align(Alignment.End) + ) { + Button( + modifier = Modifier.padding(end = 5.dp), + onClick = { + operatingTimeDialog.closeDialog() + }) { + Text(stringResource(id = R.string.cancel)) + } + Button( + onClick = { + operatingTimeDialog.closeDialog() + operatingTimeDialog.onSettingStoreTime(Pair(timeValue, operatingTimeDialog.dayOfWeekIndex)) + }) { + Text(stringResource(id = R.string.positive)) + } } - ) - }, - confirmButton = { - Button( - onClick = { - operatingTimeDialog.onSettingStoreTime(Pair(closeTimeValue, operatingTimeDialog.dayOfWeekIndex)) - }) { - Text(stringResource(id = R.string.positive)) - } - }, - dismissButton = { - Button( - onClick = { - operatingTimeDialog.closeDialog() - }) { - Text(stringResource(id = R.string.cancel)) } } - ) + } } } - @Preview @Composable -fun PreviewDayOperatingTImeSetting() { - DayOperatingTimeSetting() +fun PreviewDialog() { + OperatingTimeSettingDialog() } @Preview diff --git a/business/src/main/java/in/koreatech/business/feature/loading/GlobalLoadingScreen.kt b/business/src/main/java/in/koreatech/business/feature/loading/GlobalLoadingScreen.kt new file mode 100644 index 000000000..4961d7c17 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/loading/GlobalLoadingScreen.kt @@ -0,0 +1,24 @@ +package `in`.koreatech.business.feature.loading + +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties + +@Composable +fun GlobalLoadingScreen() { + val isLoading = LoadingState.isLoading.collectAsState().value + + if (isLoading) { + Dialog( + onDismissRequest = { }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true, + ) + ) { + CircularProgressIndicator() + } + } +} diff --git a/business/src/main/java/in/koreatech/business/feature/loading/LoadingState.kt b/business/src/main/java/in/koreatech/business/feature/loading/LoadingState.kt new file mode 100644 index 000000000..b7e2e2d93 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/loading/LoadingState.kt @@ -0,0 +1,19 @@ +package `in`.koreatech.business.feature.loading + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +object LoadingState { + private val _isLoading = MutableStateFlow(false) + val isLoading: StateFlow = _isLoading.asStateFlow() + + fun show() { + _isLoading.value = true + } + + fun hide() { + _isLoading.value = false + } +} + 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 fc9d17152..72832ae79 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 @@ -49,14 +49,19 @@ class AccountSetupViewModel @Inject constructor( .map { it.authCode } .distinctUntilChanged() + private val authFlow = container.stateFlow + .map { it.verifyState } + .distinctUntilChanged() + init { combine( passwordFlow, passwordConfirmFlow, phoneNumberFlow, - authCodeFlow - ) { password, passwordConfirm, phoneNumber, authCode -> - password.isNotEmpty() && passwordConfirm.isNotEmpty() && phoneNumber.isNotEmpty() && authCode.isNotEmpty() + authCodeFlow, + authFlow + ) { password, passwordConfirm, phoneNumber, authCode, auth -> + password.isNotEmpty() && passwordConfirm.isNotEmpty() && phoneNumber.isNotEmpty() && authCode.isNotEmpty() && auth == SignupContinuationState.CheckComplete && password.isValidPassword() && password == passwordConfirm }.distinctUntilChanged() .onEach { @@ -70,22 +75,31 @@ class AccountSetupViewModel @Inject constructor( } } - fun onPasswordChanged(password: String) = intent { + fun onPasswordChanged(password: String) = blockingIntent { reduce { state.copy( password = password, + ) + + } + reduce{ + state.copy( isPasswordError = !password.isValidPassword(), isPasswordConfirmError = state.password != state.passwordConfirm ) } } - fun onPasswordConfirmChanged(passwordConfirm: String) = intent { + fun onPasswordConfirmChanged(passwordConfirm: String) = blockingIntent { reduce { state.copy( passwordConfirm = passwordConfirm, - isPasswordConfirmError = state.password != passwordConfirm - ) + + ) + } + reduce { + state.copy( isPasswordConfirmError = state.password != passwordConfirm) + } } 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 a7dd7cabe..104170ef0 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 @@ -76,8 +76,9 @@ fun BusinessAuthScreen( val accountSetupState = accountSetupViewModel.collectAsState().value val multiplePhotoPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickMultipleVisualMedia(5), - onResult = { uriList -> - var fileName = "" + onResult = { + businessAuthViewModel.changeImageUri(it) + /* var fileName = "" var fileSize = 0L businessAuthState.fileInfo.clear() businessAuthViewModel.initStoreImageUrls() @@ -108,7 +109,7 @@ fun BusinessAuthScreen( } inputStream?.close() - } + }*/ } ) @@ -318,9 +319,11 @@ fun BusinessAuthScreen( contentDescription = stringResource(id = R.string.attach_file) ) Text( + modifier = Modifier.padding(start = 8.dp), text = stringResource(id = R.string.file_upload), fontSize = 13.sp, fontWeight = Bold, + color = Gray1, ) } } @@ -339,16 +342,10 @@ fun BusinessAuthScreen( ), onClick = { - businessAuthViewModel.sendRegisterRequest( - fileUrls = businessAuthState.fileInfo.map { it.resultUrl }, - companyNumber = businessAuthState.shopNumber, + businessAuthViewModel.onPositiveButtonClicked(context, phoneNumber = accountSetupState.phoneNumber, - name = businessAuthState.name, - password = accountSetupState.password, - shopId = businessAuthState.shopId, - shopName = businessAuthState.shopName, - ) - + password = accountSetupState.password + ) }) { Text( text = stringResource(id = R.string.next), 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 39977f381..c84e888af 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 @@ -13,6 +13,7 @@ data class BusinessAuthState( val openAlertDialog: Boolean = false, val selectedImages :MutableList = mutableListOf(), val dialogVisibility:Boolean = false, + val imageUriList: List = emptyList(), val fileInfo: MutableList = mutableListOf(), val bitmap: MutableList = mutableListOf(), val signupContinuationState: SignupContinuationState = SignupContinuationState.RequestedSmsValidation, 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 ec0ffd44e..1e3cb8e25 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,9 +1,18 @@ package `in`.koreatech.business.feature.signup.businessauth +import android.content.Context +import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import `in`.koreatech.business.feature.loading.LoadingState +import `in`.koreatech.business.feature.storemenu.modifymenu.modifymenu.ImageHolder +import `in`.koreatech.business.feature.storemenu.modifymenu.modifymenu.TEMP_IMAGE_URI +import `in`.koreatech.business.feature.storemenu.modifymenu.modifymenu.toStringList +import `in`.koreatech.business.util.getImageInfo import `in`.koreatech.koin.data.mapper.strToOwnerRegisterUrl +import `in`.koreatech.koin.domain.constant.SIGN_UP_IMAGE_MAX +import `in`.koreatech.koin.domain.constant.STORE_MENU_IMAGE_MAX import `in`.koreatech.koin.domain.model.store.AttachStore import `in`.koreatech.koin.domain.model.store.StoreUrl import `in`.koreatech.koin.domain.state.signup.SignupContinuationState @@ -83,23 +92,77 @@ class BusinessAuthViewModel @Inject constructor( postSideEffect(BusinessAuthSideEffect.NavigateToNextScreen) } - fun getPreSignedUrl( + fun changeImageUri(uriList: List){ + intent { + reduce { + if(uriList.size < SIGN_UP_IMAGE_MAX){ + val newMenuUriList = state.imageUriList.toMutableList() + for(imageUri in uriList) { + newMenuUriList.add(imageUri.toString()) + insertStoreFileUrl(imageUri.toString().substringAfterLast("/"), imageUri.toString()) + } + if(newMenuUriList.size != SIGN_UP_IMAGE_MAX)newMenuUriList.add(ImageHolder.TempUri.toString()) + + state.copy( + imageUriList = newMenuUriList + ) + } + else{ + state.copy( + imageUriList = uriList.toStringList() + ) + } + } + } + } + + + fun onPositiveButtonClicked(context: Context, phoneNumber: String, password: String) { + intent { + viewModelScope.launch { + state.imageUriList.forEach { uriString -> + if (uriString != TEMP_IMAGE_URI) { + if (uriString.contains("content")) { + val uri = Uri.parse(uriString) + val imageInfo = getImageInfo(context, uri) + getPreSignedUrl( + fileSize = imageInfo.imageSize, + fileType = imageInfo.imageType, + fileName = imageInfo.imageName, + imageUri = uriString + ) + } + } + } + sendRegisterRequest( + fileUrls = state.fileInfo.map { it.resultUrl }, + companyNumber = state.shopNumber, + phoneNumber = phoneNumber, + name = state.name, + password = password, + shopId = state.shopId, + shopName = state.shopName, + ) + } + } + } + + private fun getPreSignedUrl( fileSize: Long, fileType: String, fileName: String, imageUri: String, ) { viewModelScope.launch { + LoadingState.show() getPresignedUrlUseCase( fileSize, fileType, fileName ).onSuccess { uploadImage( - title = fileName.substringAfterLast("/"), preSignedUrl = it.second, mediaType = fileType, mediaSize = fileSize, imageUri = imageUri, - fileUrl = it.first, ) intent { reduce { @@ -129,8 +192,6 @@ class BusinessAuthViewModel @Inject constructor( } private fun uploadImage( - title: String, - fileUrl: String, preSignedUrl: String, mediaType: String, mediaSize: Long, @@ -143,7 +204,6 @@ class BusinessAuthViewModel @Inject constructor( mediaSize, imageUri ).onSuccess { - insertStoreFileUrl(title, fileUrl) intent { reduce { state.copy(error = null) } } @@ -152,10 +212,11 @@ class BusinessAuthViewModel @Inject constructor( reduce { state.copy(error = it) } } } + LoadingState.hide() } } - fun sendRegisterRequest( + private fun sendRegisterRequest( fileUrls: List, companyNumber: String, phoneNumber: String, @@ -189,6 +250,7 @@ class BusinessAuthViewModel @Inject constructor( private fun insertStoreFileUrl(title: String, url: String) { intent { + reduce { state.copy( selectedImages = state.selectedImages.apply { 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 index 26721e314..9d4f69bfa 100644 --- 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 @@ -138,7 +138,8 @@ fun CheckTermScreen( } Column( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() .padding(horizontal = 24.dp) .verticalScroll(scrollState), verticalArrangement = Arrangement.Center, @@ -215,7 +216,7 @@ fun CheckTermScreen( .height(143.dp) .verticalScroll(scrollStatePrivacy) ) { - Text(text = stringResource(R.string.term_1), fontSize = 10.sp, color = Color.Black) + Text(text = state.privacyTerm, fontSize = 10.sp, color = Color.Black) } Spacer(modifier = Modifier.height(15.dp)) @@ -258,7 +259,7 @@ fun CheckTermScreen( .height(143.dp) .verticalScroll(scrollStateKoin) ) { - Text(text = stringResource(R.string.term_2), fontSize = 10.sp, color = Color.Black) + Text(text = state.koinTerm, fontSize = 10.sp, color = Color.Black) } Spacer(modifier = Modifier.weight(1f)) 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 index e69678393..a75998919 100644 --- 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 @@ -3,5 +3,8 @@ package `in`.koreatech.business.feature.signup.checkterm data class CheckTermState( val isAllTermChecked: Boolean = false, val isCheckedPrivacyTerms: Boolean = false, - val isCheckedKoinTerms: Boolean = false + val isCheckedKoinTerms: Boolean = false, + val privacyTerm: String = "", + val koinTerm: String = "", + val throwable: Throwable? = null, ) \ 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 index f159bde99..c874e44cd 100644 --- 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 @@ -1,7 +1,11 @@ package `in`.koreatech.business.feature.signup.checkterm import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import `in`.koreatech.koin.domain.usecase.signup.GetKoinTermTextUseCase +import `in`.koreatech.koin.domain.usecase.signup.GetPrivacyTermTextUseCase +import kotlinx.coroutines.launch import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.postSideEffect @@ -11,11 +15,19 @@ import javax.inject.Inject @HiltViewModel -class CheckTermViewModel @Inject constructor() : +class CheckTermViewModel @Inject constructor( + private val getKoinTermUseCase: GetKoinTermTextUseCase, + private val getPrivacyTermUseCase: GetPrivacyTermTextUseCase, +) : ContainerHost, ViewModel() { override val container = container(CheckTermState()) + init { + loadKoinTerm() + loadPrivacyTerm() + } + fun onAllTermCheckedChanged() { intent { reduce { @@ -56,6 +68,33 @@ class CheckTermViewModel @Inject constructor() : } } + fun loadKoinTerm() = intent { + viewModelScope.launch { + getKoinTermUseCase() + .onSuccess { + reduce { state.copy(koinTerm = it) } + } + .onFailure { + reduce { state.copy(throwable = it) } + } + } + + + } + + fun loadPrivacyTerm() = intent { + viewModelScope.launch { + getPrivacyTermUseCase() + .onSuccess { + reduce { state.copy(privacyTerm = it) } + } + .onFailure { + reduce { state.copy(throwable = it) } + } + } + + } + fun onNextButtonClicked() { intent { postSideEffect(CheckTermSideEffect.NavigateToNextScreen) @@ -67,4 +106,4 @@ class CheckTermViewModel @Inject constructor() : postSideEffect(CheckTermSideEffect.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 34f2e317a..3f90574bd 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 @@ -32,6 +32,7 @@ import `in`.koreatech.business.R 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.Gray6 @Composable @@ -67,6 +68,7 @@ fun BusinessAlertDialog( fontSize = 18.sp, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, + color = Color.Black, ) Spacer(modifier = Modifier.height(8.dp)) @@ -85,7 +87,7 @@ fun BusinessAlertDialog( onClick = { onDismissRequest() }, - border = BorderStroke(1.dp, ColorActiveButton), + border = BorderStroke(1.dp, Gray6), shape = RoundedCornerShape(8.dp), modifier = Modifier .width(128.dp) @@ -93,7 +95,7 @@ fun BusinessAlertDialog( ) { Text( textAlign = TextAlign.Center, - text = stringResource(id = R.string.cancel), + text = stringResource(id = R.string.cancel_upload), color = ColorMinor, ) } diff --git a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoScreen.kt b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoScreen.kt index d574d4aad..623b2022d 100644 --- a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoScreen.kt +++ b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoScreen.kt @@ -1,5 +1,8 @@ package `in`.koreatech.business.feature.store.modifyinfo +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -15,6 +18,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults @@ -38,7 +43,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.rememberAsyncImagePainter import `in`.koreatech.business.R +import `in`.koreatech.business.feature.launchImagePicker import `in`.koreatech.business.feature.store.storedetail.MyStoreDetailViewModel import `in`.koreatech.business.ui.theme.ColorMinor import `in`.koreatech.business.ui.theme.ColorPrimary @@ -46,11 +53,13 @@ import `in`.koreatech.business.ui.theme.ColorSecondary import `in`.koreatech.business.ui.theme.Gray2 import `in`.koreatech.business.ui.theme.Gray6 import `in`.koreatech.business.ui.theme.Gray9 +import `in`.koreatech.koin.core.toast.ToastUtil import `in`.koreatech.koin.domain.util.DateFormatUtil.dayOfWeekToIndex import `in`.koreatech.koin.domain.util.StoreUtil import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect +@OptIn(ExperimentalFoundationApi::class) @Composable fun ModifyInfoScreen( modifier: Modifier = Modifier, @@ -64,14 +73,28 @@ fun ModifyInfoScreen( val storeInfoState = storeInfoViewModel.collectAsState().value val listState = rememberLazyListState() val context = LocalContext.current - + val pagerState = rememberPagerState { state.storeInfo.imageUrls.size } + val galleryLauncher = launchImagePicker( + contentResolver = context.contentResolver, + initImageUrls = viewModel::initStoreImageUrls, + getPreSignedUrl = { + viewModel.getPreSignedUrl( + it.first.first, + it.first.second, + it.second.first, + it.second.second + ) + }, + clearFileInfo = { state.fileInfo.clear() } + ) + Column { Box( modifier = Modifier .fillMaxWidth() .background(ColorPrimary), ) { - IconButton(onClick = { viewModel.onBackButtonClicked() }) { + IconButton(onClick = viewModel::onBackButtonClicked) { Image( painter = painterResource(id = R.drawable.ic_flyer_before_arrow), contentDescription = stringResource(R.string.back), @@ -97,15 +120,38 @@ fun ModifyInfoScreen( .background(Gray2), contentAlignment = Alignment.Center, ) { - Image( - modifier = Modifier.height(255.dp), - painter = state.storeInfo?.imageUrls?.getOrNull(0) - .let { painterResource(id = R.drawable.no_image) }, - contentDescription = stringResource(R.string.shop_image), - contentScale = ContentScale.Crop, - ) + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .height(255.dp) + .align(Alignment.Center), + ) { page -> + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier.height(255.dp), + painter = rememberAsyncImagePainter( + model = if (state.storeInfo.imageUrls.isNotEmpty()) state.storeInfo.imageUrls[page] else R.drawable.no_image + ), + contentDescription = stringResource(R.string.shop_image), + contentScale = ContentScale.Crop, + ) + } + } + Button( - onClick = {}, + onClick = { + galleryLauncher.launch( + PickVisualMediaRequest( + ActivityResultContracts.PickVisualMedia.ImageOnly + ) + ) + + }, modifier = Modifier .align(Alignment.BottomEnd) .width(100.dp) @@ -284,6 +330,10 @@ fun ModifyInfoScreen( storeInfoViewModel.refreshStoreList() onModifyButtonClicked() } + ModifyInfoSideEffect.ShowToastMessage -> { + ToastUtil.getInstance().makeShort(R.string.error_image_upload) + } + else -> {} } } } @@ -348,5 +398,4 @@ fun AvailableRadioButton(text: String, selected: Boolean, onClick: () -> Unit) { ), ) } - -} \ No newline at end of file +} diff --git a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoSideEffect.kt b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoSideEffect.kt index a1d711d1f..5d930734c 100644 --- a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoSideEffect.kt +++ b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoSideEffect.kt @@ -4,4 +4,5 @@ sealed class ModifyInfoSideEffect { data object NavigateToBackScreen : ModifyInfoSideEffect() data object NavigateToSettingOperatingTime : ModifyInfoSideEffect() data object NavigateToMyStoreScreen : ModifyInfoSideEffect() + data object ShowToastMessage: ModifyInfoSideEffect() } diff --git a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoState.kt b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoState.kt index 98f50b10e..9987dcd80 100644 --- a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoState.kt +++ b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoState.kt @@ -1,7 +1,11 @@ package `in`.koreatech.business.feature.store.modifyinfo import com.chargemap.compose.numberpicker.FullHours +import com.chargemap.compose.numberpicker.Hours +import `in`.koreatech.business.feature.insertstore.insertdetailinfo.operatingTime.OperatingTimeState +import `in`.koreatech.koin.domain.model.owner.SettingTime import `in`.koreatech.koin.domain.model.owner.StoreDetailInfo +import `in`.koreatech.koin.domain.model.owner.insertstore.OperatingTime import `in`.koreatech.koin.domain.model.store.AttachStore import `in`.koreatech.koin.domain.model.store.StoreUrl @@ -21,8 +25,9 @@ data class ModifyInfoState( bank = "", accountNumber = "", ), + val operatingTimeList: List = listOf(), val fileInfo: MutableList = mutableListOf(), - val dialogTimeState: OperatingTime = OperatingTime(FullHours(0, 0), FullHours(0, 0)), val showDialog: Boolean = false, val dayOfWeekIndex: Int = -1, + val isOpenTimeSetting: SettingTime = SettingTime.OPEN, ) diff --git a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoViewModel.kt b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoViewModel.kt index 694f67aea..167ce54e1 100644 --- a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoViewModel.kt +++ b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyInfoViewModel.kt @@ -6,12 +6,14 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.chargemap.compose.numberpicker.FullHours +import com.chargemap.compose.numberpicker.Hours import dagger.hilt.android.lifecycle.HiltViewModel +import `in`.koreatech.koin.domain.model.owner.SettingTime import `in`.koreatech.koin.domain.model.owner.StoreDetailInfo import `in`.koreatech.koin.domain.model.store.StoreUrl import `in`.koreatech.koin.domain.usecase.business.UploadFileUseCase import `in`.koreatech.koin.domain.usecase.business.store.ModifyShopInfoUseCase -import `in`.koreatech.koin.domain.usecase.owner.GetPresignedUrlUseCase +import `in`.koreatech.koin.domain.usecase.presignedurl.GetMarketPreSignedUrlUseCase import kotlinx.coroutines.launch import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.blockingIntent @@ -23,7 +25,7 @@ import javax.inject.Inject @HiltViewModel class ModifyInfoViewModel @Inject constructor( - private val getPresignedUrlUseCase: GetPresignedUrlUseCase, + private val getPresignedUrlUseCase: GetMarketPreSignedUrlUseCase, private val uploadFilesUseCase: UploadFileUseCase, private val modifyInfoUseCase: ModifyShopInfoUseCase, ) : ViewModel(), @@ -34,6 +36,43 @@ class ModifyInfoViewModel @Inject constructor( reduce { state.copy(storeInfo = storeInfo) } + initStoreTimeList() + } + + private fun initStoreTimeList() { + intent{ + reduce{ + val newList = state.storeInfo.operatingTime.toMutableList() + state.copy( + operatingTimeList = newList + ) + } + } + } + + private fun isOpenTimeSetting(openTimeSetting: SettingTime) = intent{ + reduce{ + state.copy(isOpenTimeSetting = openTimeSetting) + } + } + + private fun dayOfIndex(index: Int) = intent{ + reduce{ + state.copy(dayOfWeekIndex = index) + } + } + + private fun modifyStoreTime(){ + intent { + val newList = state.operatingTimeList.toMutableList() + reduce { + state.copy( + storeInfo = state.storeInfo.copy( + operatingTime = newList + ) + ) + } + } } fun onBackButtonClicked() = intent { @@ -44,61 +83,65 @@ class ModifyInfoViewModel @Inject constructor( postSideEffect(ModifyInfoSideEffect.NavigateToSettingOperatingTime) } - fun showAlertDialog(index: Int) = intent { + fun hideAlertDialog() = intent { reduce { state.copy( - showDialog = true, - dayOfWeekIndex = index, + showDialog = false, ) } } - fun hideAlertDialog() = intent { - reduce { - state.copy( - showDialog = false, - ) + fun settingStoreOpenTime(time: Hours, index: Int) { + intent { + if (index >= 0 && index < state.operatingTimeList.size) { + val newList = state.operatingTimeList.toMutableList() + val currentItem = newList[index] + newList[index] = currentItem.copy(openTime = time.toTimeString()) + + reduce { + state.copy(operatingTimeList = newList) + } + modifyStoreTime() + closeDialog() + } + } + } + + fun settingStoreCloseTime(time: Hours, index: Int) { + intent { + if (index >= 0 && index < state.operatingTimeList.size) { + val newList = state.operatingTimeList.toMutableList() + val currentItem = newList[index] + newList[index] = currentItem.copy(closeTime = time.toTimeString()) + + reduce { + state.copy(operatingTimeList = newList) + } + modifyStoreTime() + closeDialog() + } } } - fun initDialogTimeSetting(openTime: String, closeTime: String) = intent { + fun showOpenTimeDialog(index: Int) = intent{ reduce { - if (state.dayOfWeekIndex < 0) return@reduce state - val openTimeParts = openTime.split(":").map { it.toInt() } - val closeTimeParts = closeTime.split(":").map { it.toInt() } - state.copy( - dialogTimeState = OperatingTime( - FullHours(openTimeParts[0], openTimeParts[1]), - FullHours(closeTimeParts[0], closeTimeParts[1]) - ) - ) + state.copy(showDialog = true) } + isOpenTimeSetting(SettingTime.OPEN) + dayOfIndex(index) } - fun onSettingStoreTime(openTime: FullHours, closeTime: FullHours) = intent { + fun showCloseTimeDialog(index: Int) = intent{ reduce { - state.copy(dialogTimeState = OperatingTime(openTime, closeTime), - storeInfo = state.storeInfo.copy( - operatingTime = state.storeInfo.operatingTime.mapIndexed { index, operatingTime -> - if (index == state.dayOfWeekIndex) { - operatingTime.copy( - openTime = String.format( - "%02d:%02d", - openTime.hours, - openTime.minutes - ), - closeTime = String.format( - "%02d:%02d", - closeTime.hours, - closeTime.minutes - ) - ) - } else { - operatingTime - } - } - ) - ) + state.copy(showDialog = true) + } + isOpenTimeSetting(SettingTime.CLOSE) + dayOfIndex(index) + } + + private fun closeDialog() = intent{ + reduce{ + state.copy(showDialog = false) } } @@ -189,26 +232,31 @@ class ModifyInfoViewModel @Inject constructor( } } + fun getPreSignedUrl( - uri: Uri, fileSize: Long, fileType: String, fileName: String, + imageUri: String ) { viewModelScope.launch { getPresignedUrlUseCase( fileSize, fileType, fileName ).onSuccess { + uploadImage( + preSignedUrl = it.second, + mediaType = fileType, + mediaSize = fileSize, + imageUri = imageUri, + fileUrl = it.first, + ) intent { reduce { state.copy( - storeInfo = state.storeInfo.copy( - - ), fileInfo = state.fileInfo.toMutableList().apply { add( StoreUrl( - uri.toString(), + imageUri, it.first, fileName, fileType, @@ -217,31 +265,72 @@ class ModifyInfoViewModel @Inject constructor( ) ) }, - ) } } }.onFailure { intent { + postSideEffect(ModifyInfoSideEffect.ShowToastMessage) } } } } - fun uploadImage( - url: String, - imageUri: String, + private fun uploadImage( + fileUrl: String, + preSignedUrl: String, mediaType: String, - mediaSize: Long + mediaSize: Long, + imageUri: String ) { - viewModelScope.launch{ - uploadFilesUseCase(url, imageUri, mediaSize, mediaType).onSuccess { - intent { - } + viewModelScope.launch { + uploadFilesUseCase( + preSignedUrl, + mediaType, + mediaSize, + imageUri + ).onSuccess { + insertStoreFileUrl(fileUrl) }.onFailure { intent { + postSideEffect(ModifyInfoSideEffect.ShowToastMessage) } } } } + + private fun insertStoreFileUrl(url: String) { + intent { + reduce { + state.copy( + storeInfo = state.storeInfo.copy( + imageUrls = state.storeInfo.imageUrls.toMutableList().apply { + add(url) + } + ), + ) + } + } + } + + fun initStoreImageUrls() = intent { + reduce { + state.copy( + storeInfo = state.storeInfo.copy( + imageUrls = emptyList() + ) + ) + } + } +} + +private fun Hours.toTimeString(): String { + + val hoursString: String = + if (this.hours < 10) "0" + this.hours.toString() else this.hours.toString() + + val minutesString: String = + if (this.minutes < 10) "0" + this.minutes.toString() else this.minutes.toString() + + return "$hoursString:$minutesString" } diff --git a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyOperatingTimeScreen.kt b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyOperatingTimeScreen.kt index 83f3b0ae0..426243ebd 100644 --- a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyOperatingTimeScreen.kt +++ b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/ModifyOperatingTimeScreen.kt @@ -28,12 +28,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import `in`.koreatech.business.feature.store.storedetail.MyStoreDetailViewModel import `in`.koreatech.business.ui.theme.ColorMinor import `in`.koreatech.business.ui.theme.ColorPrimary import `in`.koreatech.business.ui.theme.ColorSecondaryText import `in`.koreatech.business.ui.theme.ColorTextBackgrond import `in`.koreatech.koin.core.R +import `in`.koreatech.koin.domain.model.owner.SettingTime import `in`.koreatech.koin.domain.model.owner.insertstore.OperatingTime import `in`.koreatech.koin.domain.util.DateFormatUtil import org.orbitmvi.orbit.compose.collectAsState @@ -41,7 +41,6 @@ import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun ModifyOperatingTimeScreen( - modifier: Modifier = Modifier, viewModel: ModifyInfoViewModel = hiltViewModel(), onBackClicked: () -> Unit = {}, ) { @@ -113,12 +112,14 @@ fun ModifyOperatingTimeScreen( .padding(top = 25.dp) .padding(horizontal = 6.dp) ) { - itemsIndexed(state.storeInfo.operatingTime) { index, item -> + itemsIndexed(state.operatingTimeList) { index, item -> OperatingTimeSetting( state = state, onShowOpenTimeDialog = { - viewModel.showAlertDialog(index) - viewModel.initDialogTimeSetting(item.openTime, item.closeTime) + viewModel.showOpenTimeDialog(index) + }, + onShowCloseTimeDialog = { + viewModel.showCloseTimeDialog(index) }, operatingTime = item, index = index, @@ -127,14 +128,36 @@ fun ModifyOperatingTimeScreen( } } } - OperatingTimeSettingDialog( - onDismiss = viewModel::hideAlertDialog, - onSettingStoreTime = viewModel::onSettingStoreTime, - visibility = state.showDialog, - operatingTime = state.dialogTimeState - ) - } + when(state.isOpenTimeSetting){ + SettingTime.OPEN ->{ + OperatingTimeSettingDialog( + title = stringResource(id = R.string.store_open_time), + operatingTimeDialog = OperatingTimeDialog( + state.showDialog, + viewModel::hideAlertDialog, + state.dayOfWeekIndex, + onSettingStoreTime = { + viewModel.settingStoreOpenTime(it.first, it.second) + } + ) + ) + } + SettingTime.CLOSE ->{ + OperatingTimeSettingDialog( + title = stringResource(id = R.string.store_close_time), + operatingTimeDialog = OperatingTimeDialog( + state.showDialog, + viewModel::hideAlertDialog, + state.dayOfWeekIndex, + onSettingStoreTime = { + viewModel.settingStoreCloseTime(it.first, it.second) + } + ) + ) + } + } + } } viewModel.collectSideEffect { @@ -154,6 +177,7 @@ fun OperatingTimeSetting( state: ModifyInfoState, operatingTime: OperatingTime, onShowOpenTimeDialog: (Int) -> Unit = {}, + onShowCloseTimeDialog: (Int) -> Unit = {}, index: Int = 0, onCheckBoxClicked: (Int) -> Unit = {} ) { @@ -172,12 +196,22 @@ fun OperatingTimeSetting( Spacer(modifier = Modifier.weight(1f)) Text( modifier = Modifier.clickable { - if (!state.storeInfo.operatingTime.get(index).closed) - onShowOpenTimeDialog(index) + if(!state.storeInfo.operatingTime[index].closed) onShowOpenTimeDialog(index) + }, + text = state.operatingTimeList[index].openTime, + color = if (state.storeInfo.operatingTime[index].closed) ColorMinor else Color.Black, + fontSize = 15.sp + ) + Text( + modifier = Modifier.padding(horizontal = 15.dp), + text = " ~ ", + fontSize = 15.sp + ) + Text( + modifier = Modifier.clickable { + if(!operatingTime.closed) onShowCloseTimeDialog(index) }, - text = "${state.storeInfo.operatingTime[index].openTime} ~ ${ - state.storeInfo.operatingTime[index].closeTime - }", + text = state.operatingTimeList[index].closeTime, color = if (state.storeInfo.operatingTime[index].closed) ColorMinor else Color.Black, fontSize = 15.sp ) diff --git a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/OperatingTimeDialog.kt b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/OperatingTimeDialog.kt new file mode 100644 index 000000000..861d1c06c --- /dev/null +++ b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/OperatingTimeDialog.kt @@ -0,0 +1,10 @@ +package `in`.koreatech.business.feature.store.modifyinfo + +import com.chargemap.compose.numberpicker.Hours + +data class OperatingTimeDialog( + val showDialog: Boolean = true, + val closeDialog: () -> Unit = {}, + val dayOfWeekIndex: Int = 0, + val onSettingStoreTime: (Pair) -> Unit = {} +) \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/OperatingTimeSettingDialog.kt b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/OperatingTimeSettingDialog.kt index 21a98859d..50dec34e6 100644 --- a/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/OperatingTimeSettingDialog.kt +++ b/business/src/main/java/in/koreatech/business/feature/store/modifyinfo/OperatingTimeSettingDialog.kt @@ -1,133 +1,108 @@ package `in`.koreatech.business.feature.store.modifyinfo import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight 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.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.AlertDialog import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface 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.graphics.Color import androidx.compose.ui.res.stringResource 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.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.transition.Visibility import com.chargemap.compose.numberpicker.FullHours +import com.chargemap.compose.numberpicker.Hours import com.chargemap.compose.numberpicker.HoursNumberPicker -import `in`.koreatech.business.ui.theme.Blue3 -import `in`.koreatech.business.ui.theme.Gray10 import `in`.koreatech.koin.core.R -import org.orbitmvi.orbit.compose.collectAsState @Composable fun OperatingTimeSettingDialog( - onSettingStoreTime: (FullHours, FullHours) -> Unit, - onDismiss: () -> Unit, - visibility: Boolean, - operatingTime: OperatingTime, + title: String = "", + operatingTimeDialog: OperatingTimeDialog = OperatingTimeDialog() ) { - if (visibility) { - AlertDialog(modifier = Modifier - .fillMaxWidth() - .height(300.dp), - onDismissRequest = onDismiss, - text = { - Row( - modifier = Modifier.fillMaxSize(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + var timeValue by remember { mutableStateOf(FullHours(0, 0)) } + + if (operatingTimeDialog.showDialog) { + Dialog(onDismissRequest = { operatingTimeDialog.closeDialog() }) { + Surface( + shape = RoundedCornerShape(16.dp), + color = Color.White, + elevation = 8.dp + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() ) { - HoursNumberPicker(modifier = Modifier.weight(1f), - dividersColor = MaterialTheme.colors.primary, - leadingZero = true, - value = FullHours( - operatingTime.openTime.hours, - operatingTime.openTime.minutes - ), - onValueChange = { - onSettingStoreTime( - FullHours(it.hours, it.minutes), - FullHours( - operatingTime.closeTime.hours, - operatingTime.closeTime.minutes - ) - ) - }, - minutesRange = (0..59 step 5), - hoursDivider = { - Text( - textAlign = TextAlign.Center, text = ":" - ) - }) Text( - modifier = Modifier - .weight(0.3f) - .fillMaxHeight() - .wrapContentWidth(align = Alignment.CenterHorizontally), - text = " \n\n\n\n\n~", - fontSize = 15.sp, - textAlign = TextAlign.Center + modifier = Modifier.padding(bottom = 8.dp), + text = title, + style = MaterialTheme.typography.h6, + fontSize = 20.sp ) - HoursNumberPicker(modifier = Modifier.weight(1f), + HoursNumberPicker( + modifier = Modifier + .height(120.dp), dividersColor = MaterialTheme.colors.primary, leadingZero = true, - value = FullHours( - operatingTime.closeTime.hours, - operatingTime.closeTime.minutes - ), + value = timeValue, onValueChange = { - onSettingStoreTime( - FullHours( - operatingTime.openTime.hours, - operatingTime.openTime.minutes - ), - FullHours(it.hours, it.minutes), - - ) + timeValue = it }, minutesRange = (0..59 step 5), hoursDivider = { Text( - textAlign = TextAlign.Center, text = ":" + modifier = Modifier.size(24.dp), + textAlign = TextAlign.Center, + text = ":" ) - }) - } - }, - confirmButton = { - Button( - colors = ButtonDefaults.buttonColors( - backgroundColor = Color.Transparent, - contentColor = Blue3, - ), - onClick = onDismiss, - elevation = ButtonDefaults.elevation(defaultElevation = 0.dp) - ) { - Text(stringResource(id = R.string.positive)) - } - }, - dismissButton = { - Button( - colors = ButtonDefaults.buttonColors( - backgroundColor = Color.Transparent, - contentColor = Gray10, - ), - onClick = onDismiss, - elevation = ButtonDefaults.elevation(defaultElevation = 0.dp) - ) { - Text(stringResource(id = R.string.close)) + } + ) + Row( + modifier = Modifier + .align(Alignment.End) + ) { + Button( + modifier = Modifier.padding(end = 5.dp), + onClick = { + operatingTimeDialog.closeDialog() + }) { + Text(stringResource(id = R.string.cancel)) + } + Button( + onClick = { + operatingTimeDialog.closeDialog() + operatingTimeDialog.onSettingStoreTime(Pair(timeValue, operatingTimeDialog.dayOfWeekIndex)) + }) { + Text(stringResource(id = R.string.positive)) + } + } } } - ) + } } -} +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/feature/store/storedetail/MyStoreTopBar.kt b/business/src/main/java/in/koreatech/business/feature/store/storedetail/MyStoreTopBar.kt index ee5ade8d9..22206fd92 100644 --- a/business/src/main/java/in/koreatech/business/feature/store/storedetail/MyStoreTopBar.kt +++ b/business/src/main/java/in/koreatech/business/feature/store/storedetail/MyStoreTopBar.kt @@ -3,6 +3,7 @@ package `in`.koreatech.business.feature.store.storedetail import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -17,6 +18,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Divider @@ -91,11 +94,14 @@ fun CollapsedTopBar( } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun StoreInfoScreen( viewModel: MyStoreDetailViewModel, ) { val state = viewModel.collectAsState().value + val pagerState = rememberPagerState { state.storeInfo?.imageUrls?.size ?: 1 } + Column(modifier = Modifier) { Row( @@ -189,14 +195,27 @@ fun StoreInfoScreen( .background(Gray2), contentAlignment = Alignment.Center, ) { - Image( - modifier = Modifier.height(255.dp), - painter = rememberAsyncImagePainter( - model = state.storeInfo?.imageUrls?.getOrNull(0) ?: R.drawable.no_image - ), - contentDescription = stringResource(R.string.shop_image), - contentScale = ContentScale.Crop, - ) + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .height(255.dp) + ) { page -> + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier.height(255.dp), + painter = rememberAsyncImagePainter( + model = if (state.storeInfo != null) state.storeInfo.imageUrls[page] else R.drawable.no_image + ), + contentDescription = stringResource(R.string.shop_image), + contentScale = ContentScale.Crop, + ) + } + } } Button( modifier = Modifier diff --git a/business/src/main/java/in/koreatech/business/main/BusinessMainActivity.kt b/business/src/main/java/in/koreatech/business/main/BusinessMainActivity.kt index 68cfe81a2..325580567 100644 --- a/business/src/main/java/in/koreatech/business/main/BusinessMainActivity.kt +++ b/business/src/main/java/in/koreatech/business/main/BusinessMainActivity.kt @@ -1,71 +1,73 @@ package `in`.koreatech.business.main import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels + import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface -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 dagger.hilt.android.AndroidEntryPoint +import `in`.koreatech.business.R +import `in`.koreatech.business.feature.forcrupdate.ForceUpdateScreen import `in`.koreatech.business.navigation.KoinBusinessNavHost -import `in`.koreatech.business.navigation.MYSTORESCREEN -import `in`.koreatech.business.navigation.REGISTERSTORESCREEN -import `in`.koreatech.business.navigation.SIGNINSCREEN import `in`.koreatech.business.ui.theme.KOIN_ANDROIDTheme -import `in`.koreatech.koin.domain.repository.OwnerShopRepository -import `in`.koreatech.koin.domain.repository.TokenRepository -import `in`.koreatech.koin.domain.repository.UserRepository -import javax.inject.Inject +import `in`.koreatech.koin.core.toast.ToastUtil +import `in`.koreatech.koin.domain.state.version.VersionUpdatePriority +import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @AndroidEntryPoint class BusinessMainActivity : ComponentActivity() { - - lateinit var destination: String - - private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - initViewModel() setContent { - KOIN_ANDROIDTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colors.background - ) { - KoinBusinessNavHost( - startDestination = destination - ) - } - } + KOIN_ANDROIDTheme { + KoinBusinessAppScreen() } - } - private fun initViewModel() = with(viewModel) { - destinationString.observe(this@BusinessMainActivity - ) { - destination = destinationString.value.toString() } } } + @Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { -Text( - text = "Hello $name!", - modifier = modifier -) +fun KoinBusinessAppScreen( + viewModel: BusinessMainActivityViewModel = hiltViewModel() +) { + val state = viewModel.collectAsState().value + HandleSideEffects(viewModel) + + if(state.version != null){ + when(state.version.versionUpdatePriority){ + VersionUpdatePriority.None ->{ + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colors.background + ) { + KoinBusinessNavHost( + startDestination = state.destination + ) + } + } + VersionUpdatePriority.Importance->{ + ForceUpdateScreen( + title = state.version.title, + content = state.version.content + ) + } + } + } } -@Preview(showBackground = true) @Composable -fun GreetingPreview() { -KOIN_ANDROIDTheme { - Greeting("Android") -} -} +private fun HandleSideEffects(viewModel: BusinessMainActivityViewModel) { + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + BusinessMainSideEffect.NetWorkError->{ + ToastUtil.getInstance().makeShort(R.string.version_check_failed) + } + } + } +} \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/main/BusinessMainActivitySideEffect.kt b/business/src/main/java/in/koreatech/business/main/BusinessMainActivitySideEffect.kt new file mode 100644 index 000000000..074869446 --- /dev/null +++ b/business/src/main/java/in/koreatech/business/main/BusinessMainActivitySideEffect.kt @@ -0,0 +1,5 @@ +package `in`.koreatech.business.main + +sealed class BusinessMainSideEffect { + object NetWorkError : BusinessMainSideEffect() +} diff --git a/business/src/main/java/in/koreatech/business/main/BusinessMainActivityState.kt b/business/src/main/java/in/koreatech/business/main/BusinessMainActivityState.kt index 57f4bada9..2d069ee1d 100644 --- a/business/src/main/java/in/koreatech/business/main/BusinessMainActivityState.kt +++ b/business/src/main/java/in/koreatech/business/main/BusinessMainActivityState.kt @@ -1,4 +1,8 @@ package `in`.koreatech.business.main -class BusinessMainActivityState { -} \ No newline at end of file +import `in`.koreatech.koin.domain.model.version.Version + +data class BusinessMainActivityState( + val destination: String = "", + val version: Version? = null +) diff --git a/business/src/main/java/in/koreatech/business/main/BusinessMainActivityViewModel.kt b/business/src/main/java/in/koreatech/business/main/BusinessMainActivityViewModel.kt index 1c53edd15..0ea9d0fcc 100644 --- a/business/src/main/java/in/koreatech/business/main/BusinessMainActivityViewModel.kt +++ b/business/src/main/java/in/koreatech/business/main/BusinessMainActivityViewModel.kt @@ -3,31 +3,70 @@ package `in`.koreatech.business.main import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import `in`.koreatech.koin.core.viewmodel.SingleLiveEvent +import `in`.koreatech.koin.domain.model.version.Version +import `in`.koreatech.koin.domain.state.version.VersionUpdatePriority import `in`.koreatech.koin.domain.usecase.owner.OwnerHasStoreUseCase import `in`.koreatech.koin.domain.usecase.owner.OwnerTokenIsValidUseCase +import `in`.koreatech.koin.domain.usecase.version.GetVersionInformationUseCase +import `in`.koreatech.koin.domain.usecase.version.OwnerGetVersionInformationUseCase +import `in`.koreatech.koin.domain.usecase.version.UpdateLatestVersionUseCase +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 BusinessMainActivityViewModel @Inject constructor( private val ownerTokenIsValidUseCase: OwnerTokenIsValidUseCase, private val ownerHasStoreUseCase: OwnerHasStoreUseCase, -): ViewModel() { - - private val _destinationString = MutableLiveData() - val destinationString: LiveData get() = _destinationString + private val ownerGetVersionInformationUseCase: OwnerGetVersionInformationUseCase +): ViewModel(), ContainerHost { + override val container = container(BusinessMainActivityState()) init{ ownerTokenIsValid() + checkUpdate() + } + + private fun checkUpdate() { + intent{ + viewModelScope.launch { + ownerGetVersionInformationUseCase() + .onSuccess { + reduce { + state.copy( + version = it + ) + } + }.onFailure { + postSideEffect(BusinessMainSideEffect.NetWorkError) + } + + } + } } private fun ownerTokenIsValid() { - _destinationString.value = when { - !ownerTokenIsValidUseCase() -> SIGNINSCREEN - ownerHasStoreUseCase() -> REGISTERSTORESCREEN - else -> MYSTORESCREEN + intent{ + reduce { + state.copy( + destination = when { + !ownerTokenIsValidUseCase() -> SIGNINSCREEN + ownerHasStoreUseCase() -> REGISTERSTORESCREEN + else -> MYSTORESCREEN + } + ) + } } } + + } const val SIGNINSCREEN = "sign_in_screen" 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 fb153bbcf..8b6e88cb9 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 @@ -22,10 +22,13 @@ val Gray1 = Color(0xFF4B4B4B) val Gray2= Color(0xFFEEEEEE) val Gray3= Color(0xFFCACACA) val Gray4= Color(0xFFFAFAFA) +val Gray500= Color(0xFF727272) val Gray9= Color(0xFF898A8D) val Gray10=Color(0xFF999999) val Red2 = Color(0xFFFF0000) +val ColorPrimary600 = Color(0xFF10477A) + val ColorError = Color(0xFFF05D3D) val ColorHelper = Color(0xFFD2DAE2) @@ -43,3 +46,6 @@ val ColorDescription = Color(0xFFA1A1A1) val ColorMinor = Color(0xFF858585) val ColorSearch = Color(0xFFF6F8F9) val ColorTransparency = Color(0x00FF0000) +val ColorOnCardBackground = Color(0xFF4B4B4B) +val ColorTextDescription = Color(0xFF8E8E8E) +val ColorTextFieldDescription = Color(0xFFCACACA) diff --git a/business/src/main/java/in/koreatech/business/util/GetGifImage.kt b/business/src/main/java/in/koreatech/business/util/GetGifImage.kt new file mode 100644 index 000000000..9cdbef44c --- /dev/null +++ b/business/src/main/java/in/koreatech/business/util/GetGifImage.kt @@ -0,0 +1,38 @@ +package `in`.koreatech.business.util + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import coil.ImageLoader +import coil.compose.rememberAsyncImagePainter +import coil.decode.ImageDecoderDecoder +import coil.request.ImageRequest +import coil.size.Size + + +@Composable +fun GifImage( + modifier: Modifier = Modifier, + painterResource: Int, + imageSize: Size = Size.ORIGINAL +) { + val context = LocalContext.current + val imageLoader = ImageLoader.Builder(context) + .components { + add(ImageDecoderDecoder.Factory()) + } + .build() + + Image( + modifier = modifier, + painter = rememberAsyncImagePainter( + ImageRequest.Builder(context) + .data(data = painterResource) + .apply(block = {imageSize + }).build(), + imageLoader = imageLoader + ), + contentDescription = "" + ) +} diff --git a/business/src/main/java/in/koreatech/business/util/ImageUrlExporter.kt b/business/src/main/java/in/koreatech/business/util/ImageUtil.kt similarity index 78% rename from business/src/main/java/in/koreatech/business/util/ImageUrlExporter.kt rename to business/src/main/java/in/koreatech/business/util/ImageUtil.kt index 4c185a6cf..1ef58cebe 100644 --- a/business/src/main/java/in/koreatech/business/util/ImageUrlExporter.kt +++ b/business/src/main/java/in/koreatech/business/util/ImageUtil.kt @@ -1,6 +1,7 @@ package `in`.koreatech.business.util import android.content.Context +import android.graphics.BitmapFactory import android.net.Uri import android.provider.OpenableColumns import `in`.koreatech.koin.domain.model.owner.ImageInfo @@ -32,4 +33,13 @@ suspend fun getImageInfo(context: Context, uri: Uri): ImageInfo { } imageInfo } +} + +fun getDrawableResSize(context: Context, drawableResId: Int): Pair{ + val resources = context.resources + + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeResource(resources, drawableResId, options) + + return Pair(options.outWidth, options.outHeight) } \ No newline at end of file diff --git a/business/src/main/java/in/koreatech/business/util/RefreshTokenInterceptor.kt b/business/src/main/java/in/koreatech/business/util/RefreshTokenInterceptor.kt index 7c3f04a0c..526f7aef4 100644 --- a/business/src/main/java/in/koreatech/business/util/RefreshTokenInterceptor.kt +++ b/business/src/main/java/in/koreatech/business/util/RefreshTokenInterceptor.kt @@ -30,11 +30,13 @@ class RefreshTokenInterceptor @Inject constructor( if (result.isSuccessful) { result.body()?.let { resultBody -> tokenLocalDataSource.saveAccessToken(resultBody.token) + tokenLocalDataSource.saveOwnerAccessToken(resultBody.token) tokenLocalDataSource.saveRefreshToken(resultBody.refreshToken) response = chain.proceed(getRequest(response, resultBody.token)) } } else { tokenLocalDataSource.removeAccessToken() + tokenLocalDataSource.removeOwnerAccessToken() tokenLocalDataSource.removeRefreshToken() goToLoginActivity() } diff --git a/business/src/main/res/drawable/ic_add_image.xml b/business/src/main/res/drawable/ic_add_image.xml new file mode 100644 index 000000000..b870fa7db --- /dev/null +++ b/business/src/main/res/drawable/ic_add_image.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/business/src/main/res/values/strings.xml b/business/src/main/res/values/strings.xml index bb10355e6..759c64975 100644 --- a/business/src/main/res/values/strings.xml +++ b/business/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - - BusinessMainActivity + + 코인\n사장님 사장님용\n회원가입 @@ -82,6 +82,7 @@ 사업자 등록증, 영업신고증, 통장사본을 첨부하세요. 사업자등록증, 영업신고증, 통장사본 이미지 필수\n10mb 이하의 PDF 혹은 이미지 형식의 파일(e.g. jpg, png, gif 등)로 5개까지 업로드 가능합니다. 파일 선택하기 + 취소하기 취소 사업자 등록번호 개인 연락처 @@ -162,6 +163,7 @@ 상점을 불러오지 못했습니다. 알 수 없는 오류가 발생했습니다. 상점이 존재하지 않습니다. + 이미지 업로드를 실패했습니다. 전체 이벤트/공지 수정은 중복 선택이 불가합니다. 완료 diff --git a/core/designsystem/.gitignore b/core/designsystem/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/designsystem/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts new file mode 100644 index 000000000..7f73ca58a --- /dev/null +++ b/core/designsystem/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + alias(libs.plugins.koin.library) + +} + +android { + namespace = "in.koreatech.koin.core.designsystem" + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.10" + } + + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.core.ktx) + implementation(libs.appcompat) + implementation(libs.material) + + implementation(platform(libs.compose.bom)) + implementation(libs.bundles.compose.m3) + + debugImplementation(libs.bundles.compose.debug.test) + androidTestImplementation(libs.compose.ui.test.manifest) +} \ No newline at end of file diff --git a/core/designsystem/consumer-rules.pro b/core/designsystem/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/designsystem/proguard-rules.pro b/core/designsystem/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/designsystem/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/designsystem/src/androidTest/java/in/koreatech/koin/core/designsystem/ExampleInstrumentedTest.kt b/core/designsystem/src/androidTest/java/in/koreatech/koin/core/designsystem/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..3cffd63d2 --- /dev/null +++ b/core/designsystem/src/androidTest/java/in/koreatech/koin/core/designsystem/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package `in`.koreatech.koin.core.designsystem + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("in.koreatech.koin.core.designsystem.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/AndroidManifest.xml b/core/designsystem/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/core/designsystem/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/ComposeExtensions.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/ComposeExtensions.kt new file mode 100644 index 000000000..cc0758ad0 --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/ComposeExtensions.kt @@ -0,0 +1,27 @@ +package `in`.koreatech.koin.core.designsystem + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.semantics.Role + +/** + * 리플 효과 없는 Clickable + */ +fun Modifier.noRippleClickable( + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, + onClick: () -> Unit +) = composed { + this.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + enabled = enabled, + onClickLabel = onClickLabel, + role = role, + onClick = onClick + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/button/FilledButton.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/button/FilledButton.kt new file mode 100644 index 000000000..85abe1c7b --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/button/FilledButton.kt @@ -0,0 +1,138 @@ +package `in`.koreatech.koin.core.designsystem.component.button + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme + + +enum class FilledButtonColors { + Primary, + Warning, + Danger, +} + +@Composable +fun FilledButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + textStyle: TextStyle = KoinTheme.typography.medium15, + shape: Shape = RoundedCornerShape(5.dp), + enabled: Boolean = true, + colors: FilledButtonColors = FilledButtonColors.Primary, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, +) { + val buttonColors = filledButtonColorByType(type = colors) + Button( + modifier = modifier, + onClick = onClick, + shape = shape, + enabled = enabled, + colors = buttonColors, + contentPadding = contentPadding + ) { + Text( + text = text, + style = textStyle + ) + } +} + +@Composable +fun FilledButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + textStyle: TextStyle = KoinTheme.typography.medium15, + shape: Shape = RoundedCornerShape(5.dp), + enabled: Boolean = true, + colors: ButtonColors, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, +) { + Button( + modifier = modifier, + onClick = onClick, + shape = shape, + enabled = enabled, + colors = colors, + contentPadding = contentPadding + ) { + Text( + text = text, + style = textStyle + ) + } +} + +@Composable +@Stable +internal fun filledButtonColorByType(type: FilledButtonColors): ButtonColors = when (type) { + FilledButtonColors.Primary -> ButtonColors( + containerColor = KoinTheme.colors.primary500, + contentColor = KoinTheme.colors.neutral0, + disabledContainerColor = KoinTheme.colors.neutral300, + disabledContentColor = KoinTheme.colors.neutral600 + ) + + FilledButtonColors.Warning -> ButtonColors( + containerColor = KoinTheme.colors.sub500, + contentColor = KoinTheme.colors.neutral0, + disabledContainerColor = KoinTheme.colors.neutral300, + disabledContentColor = KoinTheme.colors.neutral600 + ) + + FilledButtonColors.Danger -> ButtonColors( + containerColor = KoinTheme.colors.danger700, + contentColor = KoinTheme.colors.neutral0, + disabledContainerColor = KoinTheme.colors.neutral300, + disabledContentColor = KoinTheme.colors.neutral600 + ) +} + +@Preview +@Composable +private fun FilledButtonPreview() { + FilledButton(text = "조회하기", onClick = {}) +} + +@Preview +@Composable +private fun FilledButtonFullWidthPreview() { + FilledButton(text = "조회하기", onClick = {}, modifier = Modifier.fillMaxWidth()) +} + +@Preview +@Composable +private fun FilledButtonDisabledPreview() { + FilledButton(text = "조회하기", onClick = {}, enabled = false) +} + +@Preview +@Composable +private fun FilledButtonCustomColorPreview() { + FilledButton(text = "조회하기", onClick = {}, enabled = false, modifier = Modifier.fillMaxWidth()) +} + +@Preview +@Composable +private fun FilledButtonWarningPreview() { + FilledButton(text = "조회하기", onClick = {}, colors = FilledButtonColors.Warning) +} + +@Preview +@Composable +private fun FilledButtonDangerPreview() { + FilledButton(text = "조회하기", onClick = {}, colors = FilledButtonColors.Danger) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/button/OutlinedBoxButton.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/button/OutlinedBoxButton.kt new file mode 100644 index 000000000..34306ac69 --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/button/OutlinedBoxButton.kt @@ -0,0 +1,166 @@ +package `in`.koreatech.koin.core.designsystem.component.button + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme + + +enum class OutlinedBoxButtonColors { + Primary, + Neutral +} + +/** + * 테두리 선이 있는 텍스트 버튼 + * @param text 텍스트 + * @param onClick 버튼 클릭시 실행할 함수 + * @param textStyle 텍스트 스타일 + * @param shape 버튼 모양 + * @param enabled 버튼 활성화 여부 + * @param colors 버튼 색상 타입 + * @param contentPadding 버튼 내부 padding + * @see OutlinedBoxButtonColors + */ +@Composable +fun OutlinedBoxButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + textStyle: TextStyle = KoinTheme.typography.medium15, + shape: Shape = KoinTheme.shapes.extraSmall, + enabled: Boolean = true, + colors: OutlinedBoxButtonColors = OutlinedBoxButtonColors.Primary, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, +) { + val buttonColors = outlinedBoxButtonColorByType(colors) + val buttonBorder = outlinedBoxButtonBorderByType(colors) + + OutlinedButton( + modifier = modifier, + onClick = onClick, + enabled = enabled, + shape = shape, + colors = buttonColors, + border = buttonBorder, + contentPadding = contentPadding, + ) { + Text( + text = text, + style = textStyle + ) + } +} + + +/** + * 테두리 선이 있는 텍스트 버튼 + * @param text 텍스트 + * @param onClick 버튼 클릭시 실행할 함수 + * @param textStyle 텍스트 스타일 + * @param shape 버튼 모양 + * @param enabled 버튼 활성화 여부 + * @param colors 버튼 색상 + * @param border 테두리 선 + * @param contentPadding 버튼 내부 padding + * @see OutlinedBoxButtonColors + */ +@Composable +fun OutlinedBoxButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + textStyle: TextStyle = KoinTheme.typography.medium15, + shape: Shape = KoinTheme.shapes.extraSmall, + enabled: Boolean = true, + colors: ButtonColors = ButtonColors( + containerColor = KoinTheme.colors.neutral0, + contentColor = KoinTheme.colors.neutral0, + disabledContainerColor = KoinTheme.colors.neutral400, + disabledContentColor = KoinTheme.colors.neutral500 + ), + border: BorderStroke, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, +) { + OutlinedButton( + modifier = modifier, + onClick = onClick, + enabled = enabled, + shape = shape, + colors = colors, + border = border, + contentPadding = contentPadding, + ) { + Text( + text = text, + style = textStyle + ) + } +} + +@Composable +@Stable +internal fun outlinedBoxButtonColorByType(type: OutlinedBoxButtonColors): ButtonColors = when (type) { + OutlinedBoxButtonColors.Primary -> ButtonColors( + containerColor = KoinTheme.colors.neutral0, + contentColor = KoinTheme.colors.primary500, + disabledContainerColor = KoinTheme.colors.neutral400, + disabledContentColor = KoinTheme.colors.neutral500 + ) + + OutlinedBoxButtonColors.Neutral -> ButtonColors( + containerColor = KoinTheme.colors.neutral0, + contentColor = KoinTheme.colors.neutral500, + disabledContainerColor = KoinTheme.colors.neutral400, + disabledContentColor = KoinTheme.colors.neutral500 + ) +} + + +@Composable +@Stable +internal fun outlinedBoxButtonBorderByType(type: OutlinedBoxButtonColors): BorderStroke = when (type) { + OutlinedBoxButtonColors.Primary -> BorderStroke( + 1.0.dp, + color = KoinTheme.colors.primary500 + ) + + OutlinedBoxButtonColors.Neutral -> BorderStroke( + 1.0.dp, + color = KoinTheme.colors.neutral500 + ) +} + + +@Preview +@Composable +private fun OutlinedButtonPrimaryPreview() { + KoinTheme { + OutlinedBoxButton( + text = "대체하기", + onClick = {} + ) + } +} + +@Preview +@Composable +private fun OutlinedButtonNeutralPreview() { + KoinTheme { + OutlinedBoxButton( + text = "대체하기", + colors = OutlinedBoxButtonColors.Neutral, + onClick = {} + ) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/chip/TextChip.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/chip/TextChip.kt new file mode 100644 index 000000000..47e80d290 --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/chip/TextChip.kt @@ -0,0 +1,102 @@ +package `in`.koreatech.koin.core.designsystem.component.chip + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.noRippleClickable +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme + +/** + * 텍스트 칩 + * @param title 텍스트 + * @param isSelected 선택 여부 + * @param shape 칩 모양 + * @param showClickRipple 클릭시 리플 효과 표시 여부 + * @param onSelect 클릭시 실행할 함수 + * @param contentPadding 칩 내부 padding + * @param chipColors 칩 색상 + */ +@Composable +fun TextChip( + modifier: Modifier = Modifier, + title: String, + isSelected: Boolean = false, + shape: Shape = RoundedCornerShape(50), + showClickRipple: Boolean = true, + onSelect: () -> Unit = {}, + contentPadding: PaddingValues = PaddingValues(vertical = 6.dp, horizontal = 12.dp), + chipColors: TextChipColors = TextChipDefaults.chipColors(), +) { + Box( + modifier = modifier + .clip(shape) + .background(if(isSelected) chipColors.selectedContainerColor else chipColors.unselectedContainerColor) + .padding(contentPadding) + .then( + if (showClickRipple) Modifier.clickable { + onSelect() + } else Modifier.noRippleClickable { + onSelect() + } + ), + contentAlignment = Alignment.Center + ) { + Text( + text = title, + color = if (isSelected) chipColors.selectedContentColor else chipColors.unselectedContentColor, + ) + } +} + +object TextChipDefaults { + + @Composable + fun chipColors( + selectedContainerColor: Color = KoinTheme.colors.primary500, + selectedContentColor: Color = Color.White, + unselectedContainerColor: Color = KoinTheme.colors.neutral100, + unselectedContentColor: Color = KoinTheme.colors.neutral500, + ) = TextChipColors( + selectedContainerColor = selectedContainerColor, + selectedContentColor = selectedContentColor, + unselectedContainerColor = unselectedContainerColor, + unselectedContentColor = unselectedContentColor, + ) +} + +class TextChipColors internal constructor( + val selectedContainerColor: Color, + val selectedContentColor: Color, + val unselectedContainerColor: Color, + val unselectedContentColor: Color, +) + +@Preview +@Composable +private fun KoinTextChipPreview() { + TextChip( + title = "주중노선", + isSelected = false, + ) +} + +@Preview +@Composable +private fun KoinTextChipSelectedPreview() { + TextChip( + title = "주말노선", + isSelected = true, + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/chip/TextChipGroup.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/chip/TextChipGroup.kt new file mode 100644 index 000000000..63291454d --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/chip/TextChipGroup.kt @@ -0,0 +1,169 @@ +package `in`.koreatech.koin.core.designsystem.component.chip + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.component.chip.ChipOverflowStrategy.Flow +import `in`.koreatech.koin.core.designsystem.component.chip.ChipOverflowStrategy.Scroll + +/** + * 텍스트 칩 그룹 + * @param modifier Modifier + * @param chipOverflowStrategy Chip이 화면을 넘었을 때 전략 : [Flow], [Scroll] + * @param titles 칩 텍스트 리스트 + * @param shape 칩 모양 + * @param selectedChipIndexes 선택된 칩 인덱스 리스트 + * @param onChipSelected 칩 선택 리스너 + * @param showClickRipple 칩 클릭시 리플 효과 표시 여부 + * @param contentPadding 칩 내부 padding + * @param horizontalArrangement 칩 가로 정렬 + * @param chipColors 칩 색상 + */ +@Composable +fun TextChipGroup( + modifier: Modifier = Modifier, + chipOverflowStrategy: ChipOverflowStrategy = Flow(), + titles: List, + shape: Shape = RoundedCornerShape(50), + vararg selectedChipIndexes: Int, + onChipSelected: (title: String) -> Unit, + showClickRipple: Boolean = true, + contentPadding: PaddingValues = PaddingValues(vertical = 6.dp, horizontal = 12.dp), + horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(6.dp), + chipColors: TextChipColors = TextChipDefaults.chipColors() +) { + when (chipOverflowStrategy) { + is Flow -> KoinTextChipFlowGroup( + modifier = modifier, + titles = titles, + shape = shape, + onChipSelected = onChipSelected, + selectedChipIndexes = selectedChipIndexes, + horizontalArrangement = horizontalArrangement, + showClickRipple = showClickRipple, + contentPadding = contentPadding, + chipColors = chipColors, + verticalArrangement = chipOverflowStrategy.verticalArrangement + ) + Scroll -> KoinTextChipScrollGroup( + modifier = modifier, + titles = titles, + shape = shape, + onChipSelected = onChipSelected, + selectedChipIndexes = selectedChipIndexes, + horizontalArrangement = horizontalArrangement, + showClickRipple = showClickRipple, + contentPadding = contentPadding, + chipColors = chipColors + ) + } +} + +/** + * Chip이 화면을 넘었을 때 전략 + * @property Flow overflow된 칩들은 아래 행에 나열 + * @property Scroll 같은 행에서 계속 나열하고 스크롤 부여 + */ +sealed interface ChipOverflowStrategy { + data object Scroll : ChipOverflowStrategy + data class Flow( + val verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(6.dp) + ) : ChipOverflowStrategy +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun KoinTextChipFlowGroup( + modifier: Modifier = Modifier, + titles: List, + shape: Shape, + onChipSelected: (title: String) -> Unit, + vararg selectedChipIndexes: Int, + showClickRipple: Boolean, + contentPadding: PaddingValues, + horizontalArrangement: Arrangement.Horizontal, + verticalArrangement: Arrangement.Vertical, + chipColors: TextChipColors +) { + FlowRow( + modifier = modifier, + horizontalArrangement = horizontalArrangement, + verticalArrangement = verticalArrangement + ) { + titles.forEachIndexed { index, it -> + TextChip( + title = it, + isSelected = selectedChipIndexes.contains(index), + shape = shape, + chipColors = chipColors, + contentPadding = contentPadding, + showClickRipple = showClickRipple, + onSelect = { onChipSelected(it) } + ) + } + } +} + +@Composable +private fun KoinTextChipScrollGroup( + modifier: Modifier = Modifier, + titles: List, + shape: Shape, + onChipSelected: (title: String) -> Unit, + vararg selectedChipIndexes: Int, + showClickRipple: Boolean, + contentPadding: PaddingValues, + horizontalArrangement: Arrangement.Horizontal, + chipColors: TextChipColors +) { + Row( + modifier = modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = horizontalArrangement + ) { + titles.forEachIndexed { index, it -> + TextChip( + title = it, + isSelected = selectedChipIndexes.contains(index), + shape = shape, + chipColors = chipColors, + contentPadding = contentPadding, + showClickRipple = showClickRipple, + onSelect = { onChipSelected(it) } + ) + } + } +} + +@Preview +@Composable +private fun KoinTextChipGroupFlowPreview() { + TextChipGroup( + titles = listOf("Chip1", "Chip2", "Chip3", "Chip4", "Chip5", "Chip6", "Chip7", "Chip8"), + selectedChipIndexes = intArrayOf(0, 2, 3, 6), + onChipSelected = {}, + chipOverflowStrategy = Flow(), + chipColors = TextChipDefaults.chipColors() + ) +} + +@Preview +@Composable +private fun KoinTextChipGroupScrollPreview() { + TextChipGroup( + titles = listOf("Chip1", "Chip2", "Chip3", "Chip4", "Chip5", "Chip6", "Chip7", "Chip8"), + selectedChipIndexes = intArrayOf(0, 2, 3, 6), + onChipSelected = {}, + chipOverflowStrategy = Scroll, + chipColors = TextChipDefaults.chipColors() + ) +} diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/dialog/ChoiceDialog.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/dialog/ChoiceDialog.kt new file mode 100644 index 000000000..4d2f97fac --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/dialog/ChoiceDialog.kt @@ -0,0 +1,229 @@ +package `in`.koreatech.koin.core.designsystem.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.R +import `in`.koreatech.koin.core.designsystem.component.button.FilledButton +import `in`.koreatech.koin.core.designsystem.component.button.FilledButtonColors +import `in`.koreatech.koin.core.designsystem.component.button.OutlinedBoxButton +import `in`.koreatech.koin.core.designsystem.component.button.OutlinedBoxButtonColors +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme + + +/** + * 긍정, 부정 버튼이 있는 다이얼로그 + * @param title 다이얼로그 제목 텍스트 + * @param description 제목에 대한 설명 텍스트 + * @param onPositive 긍정 버튼 클릭시 동작할 함수 + * @param onNegative 부정 버튼 클릭시 동작할 함수 + * @param titleStyle 제목 텍스트 스타일 + * @param descriptionStyle 설명 텍스트 스타일 + * @param negativeButtonText 부정 버튼 텍스트 + * @param positiveButtonText 긍정 버튼 텍스트 + * @param positiveButtonColors 긍정 버튼 색상 + * @param negativeButtonColors 부정 버튼 색상 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChoiceDialog( + title: String, + description: String, + onPositive: () -> Unit, + onNegative: () -> Unit, + modifier: Modifier = Modifier, + titleStyle: TextStyle = KoinTheme.typography.medium18, + descriptionStyle: TextStyle = KoinTheme.typography.regular14, + positiveButtonText: String = stringResource(id = R.string.common_confirmation), + negativeButtonText: String = stringResource(id = R.string.common_cancellation), + positiveButtonColors: FilledButtonColors = FilledButtonColors.Primary, + negativeButtonColors: OutlinedBoxButtonColors = OutlinedBoxButtonColors.Neutral, +// cancellable: Boolean = true, +) { + BasicAlertDialog( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .background( + color = KoinTheme.colors.neutral0, + shape = KoinTheme.shapes.extraSmall + ) + .padding(horizontal = 32.dp, vertical = 24.dp), + onDismissRequest = { onNegative() } + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = title, + style = titleStyle + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = description, + style = descriptionStyle + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedBoxButton( + modifier = Modifier.weight(1.0F), + text = negativeButtonText, + onClick = onNegative, + colors = negativeButtonColors + ) + FilledButton( + modifier = Modifier.weight(1.0F), + text = positiveButtonText, + onClick = onPositive, + colors = positiveButtonColors + ) + } + } + } +} + + +/** + * 긍정, 부정 버튼이 있는 다이얼로그 + * @param title 다이얼로그 제목 컴포저블 + * @param description 제목에 대한 설명 컴포저블 + * @param onPositive 긍정 버튼 클릭시 동작할 함수 + * @param onNegative 부정 버튼 클릭시 동작할 함수 + * @param negativeButtonText 부정 버튼 텍스트 + * @param positiveButtonText 긍정 버튼 텍스트 + * @param positiveButtonColors 긍정 버튼 색상 + * @param negativeButtonColors 부정 버튼 색상 + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChoiceDialog( + title: @Composable () -> Unit, + description: @Composable () -> Unit, + onPositive: () -> Unit, + onNegative: () -> Unit, + modifier: Modifier = Modifier, + positiveButtonText: String = stringResource(id = R.string.common_confirmation), + negativeButtonText: String = stringResource(id = R.string.common_cancellation), + positiveButtonColors: FilledButtonColors = FilledButtonColors.Primary, + negativeButtonColors: OutlinedBoxButtonColors = OutlinedBoxButtonColors.Neutral, +// cancellable: Boolean = true, +) { + BasicAlertDialog( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .background( + color = KoinTheme.colors.neutral0, + shape = KoinTheme.shapes.extraSmall + ) + .padding(horizontal = 32.dp, vertical = 24.dp), + onDismissRequest = { onNegative() } + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + title() + Spacer(modifier = Modifier.height(8.dp)) + description() + Spacer(modifier = Modifier.height(24.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedBoxButton( + modifier = Modifier.weight(1.0F), + text = negativeButtonText, + onClick = onNegative, + colors = negativeButtonColors + ) + FilledButton( + modifier = Modifier.weight(1.0F), + text = positiveButtonText, + onClick = onPositive, + colors = positiveButtonColors + ) + } + } + } +} + +@Preview +@Composable +private fun ChoiceDialogPreview() { + KoinTheme { + ChoiceDialog( + title = "다이얼로그 제목", + description = "이러쿵 저러쿵 이러쿵 저러쿵 이러쿵 저러쿵 이러쿵 저러쿵 ", + descriptionStyle = KoinTheme.typography.regular14.copy( + color = KoinTheme.colors.neutral500 + ), + onPositive = {}, + onNegative = {}, + ) + } +} + + +@Preview +@Composable +private fun ChoiceDialogComposableTextPreview() { + KoinTheme { + ChoiceDialog( + title = { + Text( + text = buildAnnotatedString { + withStyle(KoinTheme.typography.medium16.toSpanStyle()) { + append("이렇게 ") + } + withStyle(KoinTheme.typography.bold16.toSpanStyle()) { + append("강조하는") + } + withStyle(KoinTheme.typography.medium16.toSpanStyle()) { + append(" 제목은 컴포저블로") + } + }, + textAlign = TextAlign.Center + ) + }, + description = { + Text( + text = buildAnnotatedString { + withStyle(KoinTheme.typography.regular16.toSpanStyle()) { + append("설명하는 부분도 ") + } + withStyle(KoinTheme.typography.medium16.copy(color = KoinTheme.colors.danger700).toSpanStyle()) { + append("이렇게 강조하는") + } + withStyle(KoinTheme.typography.regular16.toSpanStyle()) { + append(" 컴포저블로 추가할 수 있습니다") + } + }, + textAlign = TextAlign.Center + ) + }, + onPositive = {}, + onNegative = {}, + positiveButtonColors = FilledButtonColors.Warning + ) + } +} diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/icon/StableIcon.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/icon/StableIcon.kt new file mode 100644 index 000000000..0dcfd8400 --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/icon/StableIcon.kt @@ -0,0 +1,25 @@ +package `in`.koreatech.koin.core.designsystem.component.icon + +import androidx.annotation.DrawableRes +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource + +@Composable +fun StableIcon( + modifier: Modifier = Modifier, + @DrawableRes drawableResId: Int, + description: String? = null, + tint: Color = LocalContentColor.current, +) { + val painter = painterResource(id = drawableResId) + Icon( + painter = painter, + contentDescription = description, + tint = tint, + modifier = modifier + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/tab/KoinTabRow.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/tab/KoinTabRow.kt new file mode 100644 index 000000000..5f4720f98 --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/tab/KoinTabRow.kt @@ -0,0 +1,77 @@ +package `in`.koreatech.koin.core.designsystem.component.tab + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Tab +import androidx.compose.material3.TabPosition +import androidx.compose.material3.TabRow +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme + +/** + * 텍스트 탭 레이아웃 + */ +@Composable +fun KoinTabRow( + selectedTabIndex: Int, + onTabSelected: (index: Int) -> Unit, + titles: List, + modifier: Modifier = Modifier, + containerColor: Color = Color.White, + indicatorColor: Color = KoinTheme.colors.primary600, + selectedTextColor: Color = KoinTheme.colors.primary500, + unselectedTextColor: Color = KoinTheme.colors.neutral500, + indicator: @Composable (tabPositions: List) -> Unit = @Composable { tabPositions -> + if (selectedTabIndex < tabPositions.size) { + TabRowDefaults.SecondaryIndicator( + modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]), + color = indicatorColor + ) + } + }, + divider: @Composable () -> Unit = @Composable { + HorizontalDivider( + color = KoinTheme.colors.neutral400, + ) + } +) { + TabRow ( + selectedTabIndex = selectedTabIndex, + modifier = modifier, + containerColor = containerColor, + indicator = indicator, + divider = divider, + tabs = { + titles.forEachIndexed { index, title -> + Tab( + selected = selectedTabIndex == index, + selectedContentColor = selectedTextColor, + unselectedContentColor = unselectedTextColor, + onClick = { onTabSelected(index) }, + ) { + Text( + modifier = Modifier.padding(vertical = 12.dp), + text = title, + ) + } + } + } + ) +} + +@Preview +@Composable +private fun KoinTabRowPreview() { + KoinTabRow( + selectedTabIndex = 0, + titles = listOf("Tab1", "Tab2", "Tab3"), + onTabSelected = {}, + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/text/LeadingIconText.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/text/LeadingIconText.kt new file mode 100644 index 000000000..c9f49f9e1 --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/text/LeadingIconText.kt @@ -0,0 +1,68 @@ +package `in`.koreatech.koin.core.designsystem.component.text + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.theme.KoinTheme + +/** + * 앞에 아이콘이 있는 텍스트 + * @param modifier Modifier + * @param iconRes 아이콘 리소스 + * @param text 텍스트 + * @param textStyle 텍스트 스타일 + * @param iconSize 아이콘 크기 + * @param iconTint 아이콘 색상 + * @param spacing 아이콘과 텍스트 사이 간격 + */ +@Composable +fun LeadingIconText( + modifier: Modifier = Modifier, + @DrawableRes iconRes: Int, + text: String, + textStyle: TextStyle = KoinTheme.typography.regular14, + iconSize: Dp = 24.dp, + iconTint: Color = textStyle.color, + spacing: Dp = 8.dp +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = iconTint, + modifier = Modifier.size(iconSize) + ) + Spacer(modifier = Modifier.width(spacing)) + Text( + text = text, + style = textStyle + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun LeadingIconTextPreview() { + KoinTheme { + LeadingIconText( + iconRes = android.R.drawable.ic_media_play, + text = "예시예시예시yesi" + ) + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/topbar/KoinTopAppBar.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/topbar/KoinTopAppBar.kt new file mode 100644 index 000000000..e42b59901 --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/component/topbar/KoinTopAppBar.kt @@ -0,0 +1,61 @@ +package `in`.koreatech.koin.core.designsystem.component.topbar + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.sharp.KeyboardArrowLeft +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import `in`.koreatech.koin.core.designsystem.R +import `in`.koreatech.koin.core.designsystem.noRippleClickable + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun KoinTopAppBar( + title: String, + modifier: Modifier = Modifier, + onNavigationIconClick: () -> Unit = {}, + actions: @Composable() (RowScope.() -> Unit) = {}, + colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.White, + navigationIconContentColor = Color.Black, + titleContentColor = Color.Black, + actionIconContentColor = Color.Black, + ), +) { + CenterAlignedTopAppBar( + title = { + Text(title) + }, + modifier = modifier, + navigationIcon = { + Icon( + modifier = Modifier.size(36.dp).noRippleClickable { onNavigationIconClick() }, + imageVector = Icons.AutoMirrored.Sharp.KeyboardArrowLeft, + contentDescription = stringResource(R.string.navigate_up_content_description), + ) + }, + actions = actions, + colors = colors + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@Preview +private fun KoinTopAppBarPreview() { + KoinTopAppBar( + title = "버스 시간표", + actions = { } + ) +} \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Color.kt new file mode 100644 index 000000000..af5d1f42b --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Color.kt @@ -0,0 +1,77 @@ +package `in`.koreatech.koin.core.designsystem.theme + +import androidx.compose.ui.graphics.Color + +// primary +internal val blue90 = Color(0xFF041A44) +internal val blue80 = Color(0xFF072552) +internal val blue70 = Color(0xFF0B3566) +internal val blue60 = Color(0xFF10477A) +internal val blue50 = Color(0xFF175C8E) +internal val blue40 = Color(0xFF4590BB) +internal val blue30 = Color(0xFF6DBBDD) +internal val blue20 = Color(0xFFA2DFF3) +internal val blue10 = Color(0xFFCFF1F9) + +// sub +internal val orange90 = Color(0xFF762E05) +internal val orange80 = Color(0xFF8F3F09) +internal val orange70 = Color(0xFFB1580F) +internal val orange60 = Color(0xFFD47415) +internal val orange50 = Color(0xFFF7941E) +internal val orange40 = Color(0xFFFAB655) +internal val orange30 = Color(0xFFFCCC77) +internal val orange20 = Color(0xFFFEE1A4) +internal val orange10 = Color(0xFFFEF2D1) + +// neutral +internal val gray90 = Color(0xFF000000) +internal val gray80 = Color(0xFF1F1F1F) +internal val gray70 = Color(0xFF4B4B4B) +internal val gray60 = Color(0xFF727272) +internal val gray50 = Color(0xFFCACACA) +internal val gray40 = Color(0xFFE1E1E1) +internal val gray30 = Color(0xFFEEEEEE) +internal val gray20 = Color(0xFFF5F5F5) +internal val gray10 = Color(0xFFFAFAFA) +internal val gray5 = Color(0xFFFFFFFF) + +// danger +internal val red70 = Color(0xFFEC2D30) +internal val red60 = Color(0xFFF64C4C) +internal val red50 = Color(0xFFEB6F70) +internal val red40 = Color(0xFFF49898) +internal val red30 = Color(0xFFFFCCD2) +internal val red20 = Color(0xFFFFEBEE) +internal val red10 = Color(0xFFFEF2F2) +internal val red5 = Color(0xFFFFFBFB) + +// warning +internal val yellow70 = Color(0xFFFE9B0E) +internal val yellow60 = Color(0xFFFFAD0D) +internal val yellow50 = Color(0xFFFFC62B) +internal val yellow40 = Color(0xFFFFDD82) +internal val yellow30 = Color(0xFFFFEAB3) +internal val yellow20 = Color(0xFFFFF7E1) +internal val yellow10 = Color(0xFFFFF9EE) +internal val yellow5 = Color(0xFFFFFDFA) + +// success +internal val green70 = Color(0xFF0C9D61) +internal val green60 = Color(0xFF47B881) +internal val green50 = Color(0xFF6BC497) +internal val green40 = Color(0xFF97D4B4) +internal val green30 = Color(0xFFC0E5D1) +internal val green20 = Color(0xFFE5F5EC) +internal val green10 = Color(0xFFF2FAF6) +internal val green5 = Color(0xFFFBFEFC) + +// info +internal val skyBlue70 = Color(0xFF3A70E2) +internal val skyBlue60 = Color(0xFF3B82F3) +internal val skyBlue50 = Color(0xFF4BA1FF) +internal val skyBlue40 = Color(0xFF93C8FF) +internal val skyBlue30 = Color(0xFFBDDDFF) +internal val skyBlue20 = Color(0xFFE4F2FF) +internal val skyBlue10 = Color(0xFFF1F8FF) +internal val skyBlue5 = Color(0xFFF8FCFF) \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/ColorPalette.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/ColorPalette.kt new file mode 100644 index 000000000..bee62920e --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/ColorPalette.kt @@ -0,0 +1,193 @@ +package `in`.koreatech.koin.core.designsystem.theme + +import androidx.compose.ui.graphics.Color + +data class KoinColorPalette( + val primary900: Color, + val primary800: Color, + val primary700: Color, + val primary600: Color, + val primary500: Color, + val primary400: Color, + val primary300: Color, + val primary200: Color, + val primary100: Color, + val sub900: Color, + val sub800: Color, + val sub700: Color, + val sub600: Color, + val sub500: Color, + val sub400: Color, + val sub300: Color, + val sub200: Color, + val sub100: Color, + val neutral800: Color, + val neutral700: Color, + val neutral600: Color, + val neutral500: Color, + val neutral400: Color, + val neutral300: Color, + val neutral200: Color, + val neutral100: Color, + val neutral50: Color, + val neutral0: Color, + val danger700: Color, + val danger600: Color, + val danger500: Color, + val danger400: Color, + val danger300: Color, + val danger200: Color, + val danger100: Color, + val danger50: Color, + val warning700: Color, + val warning600: Color, + val warning500: Color, + val warning400: Color, + val warning300: Color, + val warning200: Color, + val warning100: Color, + val warning50: Color, + val success700: Color, + val success600: Color, + val success500: Color, + val success400: Color, + val success300: Color, + val success200: Color, + val success100: Color, + val success50: Color, + val info700: Color, + val info600: Color, + val info500: Color, + val info400: Color, + val info300: Color, + val info200: Color, + val info100: Color, + val info50: Color +) + +internal val KoinLightColorPalette = KoinColorPalette( + primary900 = blue90, + primary800 = blue80, + primary700 = blue70, + primary600 = blue60, + primary500 = blue50, + primary400 = blue40, + primary300 = blue30, + primary200 = blue20, + primary100 = blue10, + sub900 = orange90, + sub800 = orange80, + sub700 = orange70, + sub600 = orange60, + sub500 = orange50, + sub400 = orange40, + sub300 = orange30, + sub200 = orange20, + sub100 = orange10, + neutral800 = gray90, + neutral700 = gray80, + neutral600 = gray70, + neutral500 = gray60, + neutral400 = gray50, + neutral300 = gray40, + neutral200 = gray30, + neutral100 = gray20, + neutral50 = gray10, + neutral0 = gray5, + danger700 = red70, + danger600 = red60, + danger500 = red50, + danger400 = red40, + danger300 = red30, + danger200 = red20, + danger100 = red10, + danger50 = red5, + warning700 = yellow70, + warning600 = yellow60, + warning500 = yellow50, + warning400 = yellow40, + warning300 = yellow30, + warning200 = yellow20, + warning100 = yellow10, + warning50 = yellow5, + success700 = green70, + success600 = green60, + success500 = green50, + success400 = green40, + success300 = green30, + success200 = green20, + success100 = green10, + success50 = green5, + info700 = skyBlue70, + info600 = skyBlue60, + info500 = skyBlue50, + info400 = skyBlue40, + info300 = skyBlue30, + info200 = skyBlue20, + info100 = skyBlue10, + info50 = skyBlue5 +) + +// 다크 테마 대응시 수정 +internal val KoinDarkColorPalette = KoinColorPalette( + primary900 = blue90, + primary800 = blue80, + primary700 = blue70, + primary600 = blue60, + primary500 = blue50, + primary400 = blue40, + primary300 = blue30, + primary200 = blue20, + primary100 = blue10, + sub900 = orange90, + sub800 = orange80, + sub700 = orange70, + sub600 = orange60, + sub500 = orange50, + sub400 = orange40, + sub300 = orange30, + sub200 = orange20, + sub100 = orange10, + neutral800 = gray90, + neutral700 = gray80, + neutral600 = gray70, + neutral500 = gray60, + neutral400 = gray50, + neutral300 = gray40, + neutral200 = gray30, + neutral100 = gray20, + neutral50 = gray10, + neutral0 = gray5, + danger700 = red70, + danger600 = red60, + danger500 = red50, + danger400 = red40, + danger300 = red30, + danger200 = red20, + danger100 = red10, + danger50 = red5, + warning700 = yellow70, + warning600 = yellow60, + warning500 = yellow50, + warning400 = yellow40, + warning300 = yellow30, + warning200 = yellow20, + warning100 = yellow10, + warning50 = yellow5, + success700 = green70, + success600 = green60, + success500 = green50, + success400 = green40, + success300 = green30, + success200 = green20, + success100 = green10, + success50 = green5, + info700 = skyBlue70, + info600 = skyBlue60, + info500 = skyBlue50, + info400 = skyBlue40, + info300 = skyBlue30, + info200 = skyBlue20, + info100 = skyBlue10, + info50 = skyBlue5 +) \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Preview.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Preview.kt new file mode 100644 index 000000000..e482bd8ce --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Preview.kt @@ -0,0 +1,13 @@ + package `in`.koreatech.koin.core.designsystem.theme + +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.Preview + +@Preview(name = "Dark Mode", showBackground = true, uiMode = UI_MODE_NIGHT_YES) +@Preview(name = "Light Mode", showBackground = true, uiMode = UI_MODE_NIGHT_NO) +annotation class ThemePreviews + +@Preview(name = "Default Font size", fontScale = 1.0F) +@Preview(name = "Large Font size(1.5F)", fontScale = 1.5F) +annotation class FontScalePreviews \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Shape.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Shape.kt new file mode 100644 index 000000000..310b50828 --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Shape.kt @@ -0,0 +1,13 @@ +package `in`.koreatech.koin.core.designsystem.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +internal val Shapes = Shapes( + extraSmall = RoundedCornerShape(4.dp), + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(12.dp), + large = RoundedCornerShape(14.dp), + extraLarge = RoundedCornerShape(18.dp) +) \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Theme.kt new file mode 100644 index 000000000..5483fabb7 --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Theme.kt @@ -0,0 +1,151 @@ +package `in`.koreatech.koin.core.designsystem.theme + +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.Shapes +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.staticCompositionLocalOf + +internal val LocalKoinColorPalette = staticCompositionLocalOf { + KoinLightColorPalette +} + +internal val LocalKoinTypography = staticCompositionLocalOf { + KoinTypography( + regular10 = RegularStyle1, + regular12 = RegularStyle2, + regular13 = RegularStyle3, + regular14 = RegularStyle4, + regular15 = RegularStyle5, + regular16 = RegularStyle6, + regular18 = RegularStyle7, + medium12 = MediumStyle1, + medium13 = MediumStyle2, + medium14 = MediumStyle3, + medium15 = MediumStyle4, + medium16 = MediumStyle5, + medium18 = MediumStyle6, + bold12 = BoldStyle1, + bold13 = BoldStyle2, + bold14 = BoldStyle3, + bold15 = BoldStyle4, + bold16 = BoldStyle5, + bold18 = BoldStyle6, + bold20 = BoldStyle7, + ) +} + +internal val KoinLightColorScheme = lightColorScheme( + primary = blue50 +) + +// 다크 테마 대응시 수정 +internal val KoinDarkColorScheme = lightColorScheme( + primary = blue50 +) + +internal val LocalShapes = staticCompositionLocalOf { + Shapes +} + +@Composable +fun KoinTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicTheme: Boolean = true, + content: @Composable () -> Unit +) { + val extendedColors = + if (darkTheme) + KoinDarkColorPalette + else + KoinLightColorPalette + + val colorScheme = when { + dynamicTheme && isSupportDynamicTheme() -> { + if (darkTheme) KoinDarkColorScheme else KoinLightColorScheme + } + + darkTheme -> KoinDarkColorScheme + else -> KoinLightColorScheme + } + + val koinTypography = KoinTypography( + regular10 = RegularStyle1, + regular12 = RegularStyle2, + regular13 = RegularStyle3, + regular14 = RegularStyle4, + regular15 = RegularStyle5, + regular16 = RegularStyle6, + regular18 = RegularStyle7, + medium12 = MediumStyle1, + medium13 = MediumStyle2, + medium14 = MediumStyle3, + medium15 = MediumStyle4, + medium16 = MediumStyle5, + medium18 = MediumStyle6, + bold12 = BoldStyle1, + bold13 = BoldStyle2, + bold14 = BoldStyle3, + bold15 = BoldStyle4, + bold16 = BoldStyle5, + bold18 = BoldStyle6, + bold20 = BoldStyle7, + ) + + val typography = Typography( + displayLarge = MediumStyle6, + displayMedium = MediumStyle5, + displaySmall = MediumStyle4, + headlineLarge = MediumStyle6, + headlineMedium = MediumStyle5, + headlineSmall = MediumStyle4, + titleLarge = MediumStyle5, + titleMedium = MediumStyle4, + titleSmall = MediumStyle3, + bodyLarge = RegularStyle5, + bodyMedium = RegularStyle4, + bodySmall = RegularStyle3, + labelLarge = RegularStyle3, + labelMedium = RegularStyle2, + labelSmall = RegularStyle1 + ) + + val shapes = Shapes + + CompositionLocalProvider( + LocalKoinColorPalette provides extendedColors, + LocalKoinTypography provides koinTypography, + LocalShapes provides shapes + ) { + MaterialTheme( + colorScheme = colorScheme, + typography = typography, + shapes = Shapes, + content = content + ) + } +} + +object KoinTheme { + val colors: KoinColorPalette + @Composable + @ReadOnlyComposable + get() = LocalKoinColorPalette.current + val typography: KoinTypography + @Composable + @ReadOnlyComposable + get() = LocalKoinTypography.current + val shapes: Shapes + @Composable + @ReadOnlyComposable + get() = LocalShapes.current +} + +@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) +private fun isSupportDynamicTheme(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S \ No newline at end of file diff --git a/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Type.kt b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Type.kt new file mode 100644 index 000000000..ac43896df --- /dev/null +++ b/core/designsystem/src/main/java/in/koreatech/koin/core/designsystem/theme/Type.kt @@ -0,0 +1,161 @@ +package `in`.koreatech.koin.core.designsystem.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.unit.sp +import `in`.koreatech.koin.core.designsystem.R + +@Immutable +data class KoinTypography( + val regular10: TextStyle, + val regular12: TextStyle, + val regular13: TextStyle, + val regular14: TextStyle, + val regular15: TextStyle, + val regular16: TextStyle, + val regular18: TextStyle, + val medium12: TextStyle, + val medium13: TextStyle, + val medium14: TextStyle, + val medium15: TextStyle, + val medium16: TextStyle, + val medium18: TextStyle, + val bold12: TextStyle, + val bold13: TextStyle, + val bold14: TextStyle, + val bold15: TextStyle, + val bold16: TextStyle, + val bold18: TextStyle, + val bold20: TextStyle +) + +internal val Pretendard = FontFamily( + Font(R.font.pretendard_bold, FontWeight.Bold, FontStyle.Normal), + Font(R.font.pretendard_bold, FontWeight.W600, FontStyle.Normal), + Font(R.font.pretendard_medium, FontWeight.Medium, FontStyle.Normal), + Font(R.font.pretendard_regular, FontWeight.Normal, FontStyle.Normal), +) + +internal val DefaultTextStyle: TextStyle = TextStyle( + fontStyle = FontStyle.Normal, + fontFamily = Pretendard, + platformStyle = PlatformTextStyle( + includeFontPadding = false + ), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ), + letterSpacing = 0.sp +) + +internal val RegularStyle1 = DefaultTextStyle.copy( + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + lineHeight = 16.sp +) +internal val RegularStyle2 = DefaultTextStyle.copy( + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + lineHeight = 19.2.sp +) +internal val RegularStyle3 = DefaultTextStyle.copy( + fontSize = 13.sp, + fontWeight = FontWeight.Normal, + lineHeight = 20.8.sp +) +internal val RegularStyle4 = DefaultTextStyle.copy( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + lineHeight = 22.4.sp +) +internal val RegularStyle5 = DefaultTextStyle.copy( + fontSize = 15.sp, + fontWeight = FontWeight.Normal, + lineHeight = 24.sp +) +internal val RegularStyle6 = DefaultTextStyle.copy( + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + lineHeight = 25.6.sp +) +internal val RegularStyle7 = DefaultTextStyle.copy( + fontSize = 18.sp, + fontWeight = FontWeight.Normal, + lineHeight = 28.8.sp +) + + +internal val MediumStyle1 = DefaultTextStyle.copy( + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + lineHeight = 19.2.sp +) +internal val MediumStyle2 = DefaultTextStyle.copy( + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + lineHeight = 20.8.sp +) +internal val MediumStyle3 = DefaultTextStyle.copy( + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + lineHeight = 22.4.sp +) +internal val MediumStyle4 = DefaultTextStyle.copy( + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + lineHeight = 24.sp +) +internal val MediumStyle5 = DefaultTextStyle.copy( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + lineHeight = 25.6.sp +) +internal val MediumStyle6 = DefaultTextStyle.copy( + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + lineHeight = 28.8.sp +) + + +internal val BoldStyle1 = DefaultTextStyle.copy( + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + lineHeight = 19.2.sp +) +internal val BoldStyle2 = DefaultTextStyle.copy( + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + lineHeight = 20.8.sp +) +internal val BoldStyle3 = DefaultTextStyle.copy( + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + lineHeight = 22.4.sp +) +internal val BoldStyle4 = DefaultTextStyle.copy( + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + lineHeight = 24.sp +) +internal val BoldStyle5 = DefaultTextStyle.copy( + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + lineHeight = 25.6.sp +) +internal val BoldStyle6 = DefaultTextStyle.copy( + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + lineHeight = 28.8.sp +) +internal val BoldStyle7 = DefaultTextStyle.copy( + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + lineHeight = 30.sp +) \ No newline at end of file diff --git a/core/designsystem/src/main/res/font/pretendard_bold.otf b/core/designsystem/src/main/res/font/pretendard_bold.otf new file mode 100644 index 000000000..a52ef3991 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_bold.otf differ diff --git a/core/designsystem/src/main/res/font/pretendard_medium.otf b/core/designsystem/src/main/res/font/pretendard_medium.otf new file mode 100644 index 000000000..a2dc009f6 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_medium.otf differ diff --git a/core/designsystem/src/main/res/font/pretendard_regular.otf b/core/designsystem/src/main/res/font/pretendard_regular.otf new file mode 100644 index 000000000..c940185ae Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_regular.otf differ diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml new file mode 100644 index 000000000..1f8c96162 --- /dev/null +++ b/core/designsystem/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + 취소 + 확인 + + 뒤로가기 + \ No newline at end of file diff --git a/core/designsystem/src/test/java/in/koreatech/koin/core/designsystem/ExampleUnitTest.kt b/core/designsystem/src/test/java/in/koreatech/koin/core/designsystem/ExampleUnitTest.kt new file mode 100644 index 000000000..c2b7243e4 --- /dev/null +++ b/core/designsystem/src/test/java/in/koreatech/koin/core/designsystem/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package `in`.koreatech.koin.core.designsystem + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/core/onboarding/.gitignore b/core/onboarding/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/core/onboarding/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/onboarding/build.gradle.kts b/core/onboarding/build.gradle.kts new file mode 100644 index 000000000..b2b437109 --- /dev/null +++ b/core/onboarding/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(libs.plugins.koin.library) + alias(libs.plugins.koin.hilt) +} + +android { + namespace = "in.koreatech.koin.core.onboarding" +} +kapt { + correctErrorTypes = true +} +dependencies { + implementation(project(":domain")) + + implementation(libs.core.ktx) + implementation(libs.appcompat) + implementation(libs.timber) + implementation(libs.balloon) +} \ No newline at end of file diff --git a/core/onboarding/consumer-rules.pro b/core/onboarding/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/core/onboarding/proguard-rules.pro b/core/onboarding/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/core/onboarding/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/onboarding/src/androidTest/java/in/koreatech/koin/core/onboarding/ExampleInstrumentedTest.kt b/core/onboarding/src/androidTest/java/in/koreatech/koin/core/onboarding/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..677733208 --- /dev/null +++ b/core/onboarding/src/androidTest/java/in/koreatech/koin/core/onboarding/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package `in`.koreatech.koin.core.onboarding + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("in.koreatech.koin.core.onboarding.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/core/onboarding/src/main/AndroidManifest.xml b/core/onboarding/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/core/onboarding/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/ArrowDirection.kt b/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/ArrowDirection.kt new file mode 100644 index 000000000..5346fc6ae --- /dev/null +++ b/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/ArrowDirection.kt @@ -0,0 +1,16 @@ +package `in`.koreatech.koin.core.onboarding + +import com.skydoves.balloon.ArrowOrientation + +enum class ArrowDirection { + BOTTOM, TOP, LEFT, RIGHT; + + fun toArrowOrientation(): ArrowOrientation { + return when (this) { + BOTTOM -> ArrowOrientation.BOTTOM + TOP -> ArrowOrientation.TOP + LEFT -> ArrowOrientation.START + RIGHT -> ArrowOrientation.END + } + } +} \ No newline at end of file diff --git a/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/OnboardingManager.kt b/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/OnboardingManager.kt new file mode 100644 index 000000000..5ef18fef8 --- /dev/null +++ b/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/OnboardingManager.kt @@ -0,0 +1,152 @@ +package `in`.koreatech.koin.core.onboarding + +import android.content.Context +import android.view.View +import androidx.annotation.FloatRange +import androidx.appcompat.content.res.AppCompatResources +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.skydoves.balloon.ArrowOrientationRules +import com.skydoves.balloon.ArrowPositionRules +import com.skydoves.balloon.Balloon +import com.skydoves.balloon.BalloonAnimation +import com.skydoves.balloon.BalloonSizeSpec +import com.skydoves.balloon.IconForm +import com.skydoves.balloon.IconGravity +import `in`.koreatech.koin.domain.repository.OnboardingRepository +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +class OnboardingManager @Inject internal constructor( + private val onboardingRepository: OnboardingRepository, + private val context: Context, +) { + + private lateinit var tooltip: Balloon + private val tooltipDismissObserver = object : DefaultLifecycleObserver { + override fun onPause(owner: LifecycleOwner) { + if (::tooltip.isInitialized) + tooltip.dismiss() + super.onPause(owner) + } + } + + /** + * 앱 실행 최초 1회에만 툴팁 표시 + * @param type 툴팁 타입 + * @param view 툴팁을 위치시킬 뷰 + * @param arrowPosition 화살표 위치 (0.0 ~ 1.0) + * @param arrowDirection 툴팁 화살표 방향 (ex. ArrowDirection.LEFT -> 화살표는 왼쪽방향, 툴팁은 오른쪽에 위치) + * ``` + * // In Activity + * @Inject + * lateinit var onboardingManager: OnboardingManager + * ... + * + * with(onboardingManager) { + * showOnboardingTooltipIfNeeded( + * type = OnboardingType.DINING_IMAGE, + * view = binding.textViewDiningTitle, + * arrowDirection = ArrowDirection.LEFT + * ) + * } + * ``` + * + * binding.textViewDiningTitle 오른쪽에 툴팁 표시됨. + * + * Fragment에서 사용할 때는 viewLifecycleOwner를 사용해야 함 ! + */ + fun LifecycleOwner.showOnboardingTooltipIfNeeded( + type: OnboardingType, + view: View, + @FloatRange(from = 0.0, to = 1.0) arrowPosition: Float = 0.5f, + arrowDirection: ArrowDirection, + ) { + lifecycle.addObserver(tooltipDismissObserver) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + val shouldShow = onboardingRepository.getShouldOnboarding(type.name) + delay(200) + if (shouldShow) { + tooltip = createTooltip(type, arrowDirection, arrowPosition) + tooltip.showAlign(view, arrowDirection) + onboardingRepository.updateShouldOnboarding(type.name, false) + } + } + } + } + + /** + * 앱 최초 실행 시에 실행시키고 싶은 액션이 있는 경우 사용 + * @param action 실행시킬 액션. ex) BottomSheetDialogFragment.show() + */ + fun LifecycleOwner.showOnboardingIfNeeded( + type: OnboardingType, + action: () -> Unit + ) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + val shouldShow = onboardingRepository.getShouldOnboarding(type.name) + if (shouldShow) { + action() + onboardingRepository.updateShouldOnboarding(type.name, false) + } + } + } + } + + fun dismissTooltip() { + if (::tooltip.isInitialized) + tooltip.dismiss() + } + + private fun createTooltip( + type: OnboardingType, + arrowDirection: ArrowDirection, + arrowPosition: Float + ): Balloon { + val iconForm = IconForm.Builder(context) + .setDrawable(AppCompatResources.getDrawable(context, R.drawable.round_close_24)) + .setIconSize(32) + .setDrawableGravity(IconGravity.END) + .build() + + return Balloon.Builder(context) + .setHeight(BalloonSizeSpec.WRAP) + .setWidth(BalloonSizeSpec.WRAP) + .setTextColorResource(R.color.white) + .setBackgroundColorResource(R.color.neutral_600) + .setTextSize(12f) + .setArrowOrientationRules(ArrowOrientationRules.ALIGN_FIXED) + .setArrowOrientation(arrowDirection.toArrowOrientation()) + .setArrowPositionRules(ArrowPositionRules.ALIGN_BALLOON) + .setArrowSize(10) + .setArrowPosition(arrowPosition) + .setPaddingVertical(8) + .setPaddingHorizontal(10) + .setIconForm(iconForm) + .setMargin(10) + .setDismissWhenTouchOutside(false) + .setDismissWhenClicked(true) + .setCornerRadius(8f) + .setBalloonAnimation(BalloonAnimation.FADE) + .apply { + if (type.descriptionResId != 0) + setText(context.getString(type.descriptionResId)) + } + .build() + } + + private fun Balloon.showAlign(view: View, arrowDirection: ArrowDirection) { + when (arrowDirection) { + ArrowDirection.BOTTOM -> showAlignTop(view) + ArrowDirection.TOP -> showAlignBottom(view) + ArrowDirection.LEFT -> showAlignRight(view) + ArrowDirection.RIGHT -> showAlignLeft(view) + } + } +} \ No newline at end of file diff --git a/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/OnboardingModule.kt b/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/OnboardingModule.kt new file mode 100644 index 000000000..63573dced --- /dev/null +++ b/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/OnboardingModule.kt @@ -0,0 +1,24 @@ +package `in`.koreatech.koin.core.onboarding + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.scopes.ActivityScoped +import `in`.koreatech.koin.domain.repository.OnboardingRepository + +@Module +@InstallIn(ActivityComponent::class) +object OnboardingModule { + + @Provides + @ActivityScoped + fun provideOnboardingManager( + onboardingRepository: OnboardingRepository, + @ApplicationContext context: Context + ): OnboardingManager { + return OnboardingManager(onboardingRepository, context) + } +} \ No newline at end of file diff --git a/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/OnboardingType.kt b/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/OnboardingType.kt new file mode 100644 index 000000000..0dad8b6e7 --- /dev/null +++ b/core/onboarding/src/main/java/in/koreatech/koin/core/onboarding/OnboardingType.kt @@ -0,0 +1,16 @@ +package `in`.koreatech.koin.core.onboarding + +import androidx.annotation.StringRes + +/** + * OnboardingType + * @property descriptionResId 툴팁 내용 리소스 아이디. 툴팁이 아닐 경우 0 할당 + */ +enum class OnboardingType( + @StringRes val descriptionResId: Int, +) { + DINING_IMAGE(R.string.dining_image_tooltip), + DINING_NOTIFICATION(0), + DINING_SHARE(0), + ARTICLE_KEYWORD(R.string.article_keyword_tooltip) +} \ No newline at end of file diff --git a/core/onboarding/src/main/res/drawable/round_close_24.xml b/core/onboarding/src/main/res/drawable/round_close_24.xml new file mode 100644 index 000000000..13608b6a2 --- /dev/null +++ b/core/onboarding/src/main/res/drawable/round_close_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/onboarding/src/main/res/values/colors.xml b/core/onboarding/src/main/res/values/colors.xml new file mode 100644 index 000000000..1393e2fbd --- /dev/null +++ b/core/onboarding/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #ffffff + #4B4B4B + \ No newline at end of file diff --git a/core/onboarding/src/main/res/values/strings.xml b/core/onboarding/src/main/res/values/strings.xml new file mode 100644 index 000000000..d7ce2a42c --- /dev/null +++ b/core/onboarding/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + 식단 사진을 확인해보세요! + 식단이 마음에 들었다면 공유하기! + 키워드를 추가하고 맞춤 알림을 받아보세요! + \ No newline at end of file diff --git a/core/onboarding/src/test/java/in/koreatech/koin/core/onboarding/ExampleUnitTest.kt b/core/onboarding/src/test/java/in/koreatech/koin/core/onboarding/ExampleUnitTest.kt new file mode 100644 index 000000000..5bb1b710b --- /dev/null +++ b/core/onboarding/src/test/java/in/koreatech/koin/core/onboarding/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package `in`.koreatech.koin.core.onboarding + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/core/src/main/java/in/koreatech/koin/core/abtest/ABTest.kt b/core/src/main/java/in/koreatech/koin/core/abtest/ABTest.kt deleted file mode 100644 index bd7a6b8a4..000000000 --- a/core/src/main/java/in/koreatech/koin/core/abtest/ABTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package `in`.koreatech.koin.core.abtest - -class ABTestData( - val experimentTitle: String, // 실험 변수 - private vararg val experimentGroups: String, // 실험군 -) { - private val groupMap: Map = experimentGroups.associateWith { it } - - fun getGroup(groupName: String): String? { - return groupMap[groupName] - } -} \ No newline at end of file diff --git a/core/src/main/java/in/koreatech/koin/core/abtest/ABTestConstant.kt b/core/src/main/java/in/koreatech/koin/core/abtest/ABTestConstant.kt deleted file mode 100644 index d979da5b2..000000000 --- a/core/src/main/java/in/koreatech/koin/core/abtest/ABTestConstant.kt +++ /dev/null @@ -1,10 +0,0 @@ -package `in`.koreatech.koin.core.abtest - - -object ABTestConstants { - - val BENEFIT_STORE = ABTestData( - experimentTitle = "Benefit", - "A", "B" - ) -} \ No newline at end of file diff --git a/core/src/main/java/in/koreatech/koin/core/abtest/Experiment.kt b/core/src/main/java/in/koreatech/koin/core/abtest/Experiment.kt new file mode 100644 index 000000000..be0819c49 --- /dev/null +++ b/core/src/main/java/in/koreatech/koin/core/abtest/Experiment.kt @@ -0,0 +1,22 @@ +package `in`.koreatech.koin.core.abtest + +enum class Experiment( + val experimentTitle: String, + vararg val experimentGroups: String +) { + + BENEFIT_STORE("Benefit", ExperimentGroup.A, ExperimentGroup.B), + DINING_SHARE("campus_share_v1", ExperimentGroup.SHARE_ORIGINAL, ExperimentGroup.SHARE_NEW); + + init { + require(experimentGroups.isNotEmpty()) { "Experiment should have at least one group" } + } +} + +object ExperimentGroup { + const val A = "A" + const val B = "B" + + const val SHARE_ORIGINAL = "share_original" + const val SHARE_NEW = "share_new" +} \ No newline at end of file diff --git a/core/src/main/java/in/koreatech/koin/core/analytics/EventLogger.kt b/core/src/main/java/in/koreatech/koin/core/analytics/EventLogger.kt index e40463ad5..247dcde9b 100644 --- a/core/src/main/java/in/koreatech/koin/core/analytics/EventLogger.kt +++ b/core/src/main/java/in/koreatech/koin/core/analytics/EventLogger.kt @@ -46,16 +46,6 @@ object EventLogger { logEvent(action, EventCategory.SWIPE, label, value, *extras) } - - /** - * @param action: 이벤트 발생 도메인(BUSINESS, CAMPUS, USER) - * @param label: 이벤트 소분류 - * @param value: 이벤트 값 - */ - fun logSwipeEvent(action: EventAction, label: String, value: String) { - logEvent(action, EventCategory.SWIPE, label, value) - } - /** * @param action: 커스텀 이벤트 발생(EventAction 이외에 action) * @param category: 커스텀 이벤트 종류(EventCategory 이외에 category) diff --git a/core/src/main/java/in/koreatech/koin/core/constant/AnalyticsConstant.kt b/core/src/main/java/in/koreatech/koin/core/constant/AnalyticsConstant.kt index ba0f09c25..c1aee06ed 100644 --- a/core/src/main/java/in/koreatech/koin/core/constant/AnalyticsConstant.kt +++ b/core/src/main/java/in/koreatech/koin/core/constant/AnalyticsConstant.kt @@ -18,13 +18,14 @@ object AnalyticsConstant { const val HAMBURGER = "hamburger" const val HAMBURGER_SHOP = HAMBURGER const val HAMBURGER_DINING = "${HAMBURGER}" - const val HAMBURGER_LAND = "${HAMBURGER}" - const val HAMBURGER_MY_INFO_WITHOUT_LOGIN = "${HAMBURGER}_my_info_without_login" - const val HAMBURGER_MY_INFO_WITH_LOGIN = "${HAMBURGER}_my_info_with_login" const val HAMBURGER_BUS = "${HAMBURGER}" const val MAIN_MENU_MOVEDETAILVIEW = "main_menu_moveDetailView" const val MAIN_MENU_CORNER = "main_menu_corner" const val MENU_TIME = "menu_time" + const val BUS_TAB_MENU = "bus_tab_menu" + const val BUS_SEARCH_DEPARTURE = "bus_search_departure" + const val BUS_SEARCH_ARRIVAL = "bus_search_arrival" + const val BUS_SEARCH = "bus_search" const val MAIN_BUS = "main_bus" const val MAIN_BUS_CHANGETOFROM = "main_bus_changeToFrom" const val MAIN_BUS_SCROLL = "main_bus_scroll" @@ -32,8 +33,10 @@ object AnalyticsConstant { const val BUS_ARRIVAL = "bus_arrival" const val BUS_TIMETABLE = "bus_timetable" const val BUS_TIMETABLE_AREA = "bus_timetable_area" + const val BUS_TIMETABLE_CITYBUS_ROUTE = "bus_timetable_citybus_route" const val BUS_TIMETABLE_TIME = "bus_timetable_time" const val BUS_TIMETABLE_EXPRESS = "bus_timetable_express" + const val BUS_TIMETABLE_CITYBUS = "bus_timetable_citybus" const val MENU_IMAGE = "menu_image" const val LOGIN = "login" const val START_SIGN_UP = "start_sign_up" @@ -64,12 +67,13 @@ object AnalyticsConstant { const val SHOP_DETAIL_VIEW = "shop_detail_view" - const val SHOP_DETAIL_VIEW_REVIEW_BACK ="shop_detail_view_review_back" + const val SHOP_DETAIL_VIEW_REVIEW_BACK = "shop_detail_view_review_back" const val NOTIFICATION = "notification" const val NOTIFICATION_SOLD_OUT = "notification_sold_out" const val NOTIFICATION_BREAKFAST_SOLD_OUT = "notification_breakfast_sold_out" const val NOTIFICATION_LUNCH_SOLD_OUT = "notification_lunch_sold_out" const val NOTIFICATION_DINNER_SOLD_OUT = "notification_dinner_sold_out" + const val NOTIFICATION_MENU_IMAGE_UPLOAD = "notification_menu_image_upload" const val NOTICE_TAB = "notice_tab" const val NOTICE_PAGE = "notice_page" const val INVENTORY = "inventory" @@ -82,11 +86,15 @@ object AnalyticsConstant { const val RECOMMENDED_KEYWORD = "recommended_keyword" const val KEYWORD_NOTIFICATION = "keyword_notification" const val LOGIN_POPUP_KEYWORD = "login_popup_keyword" + const val NOTICE_SEARCH_EVENT = "notice_search_event" + const val NOTICE_ORIGINAL_SHORTCUT = "notice_original_shortcut" const val BENEFIT_SHOP_CATEGORIES = "benefit_shop_categories" const val BENEFIT_SHOP_CATEGORIES_EVENT = "benefit_shop_categories_event" const val BENEFIT_SHOP_CLICK = "benefit_shop_click" const val BENEFIT_SHOP_CALL = "benefit_shop_call" + + const val MENU_SHARE = "menu_share" } const val PREVIOUS_PAGE = "previous_page" diff --git a/core/src/main/java/in/koreatech/koin/core/util/FloatExtensions.kt b/core/src/main/java/in/koreatech/koin/core/util/FloatExtensions.kt new file mode 100644 index 000000000..12e8ed39a --- /dev/null +++ b/core/src/main/java/in/koreatech/koin/core/util/FloatExtensions.kt @@ -0,0 +1,9 @@ +package `in`.koreatech.koin.core.util + +import android.content.res.Resources +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + + +val Float.pxToDp: Dp + get() = (this / Resources.getSystem().displayMetrics.density).dp \ No newline at end of file diff --git a/core/src/main/java/in/koreatech/koin/core/util/KeyboardUtils.kt b/core/src/main/java/in/koreatech/koin/core/util/KeyboardUtils.kt new file mode 100644 index 000000000..7cef95ab7 --- /dev/null +++ b/core/src/main/java/in/koreatech/koin/core/util/KeyboardUtils.kt @@ -0,0 +1,24 @@ +package `in`.koreatech.koin.core.util + +import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager + +class KeyboardUtils( + private val context: Context +) { + // 키보드 올리기 + fun show(view: View) { + val inputMethodManager = + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + view.requestFocus() + inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) + } + + // 키보드 내리기 + fun hide(view: View) { + val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + view.clearFocus() + inputMethodManager.hideSoftInputFromWindow(view.windowToken, InputMethodManager.HIDE_IMPLICIT_ONLY) + } +} diff --git a/core/src/main/res/drawable/ic_add_image.xml b/core/src/main/res/drawable/ic_add_image.xml new file mode 100644 index 000000000..b870fa7db --- /dev/null +++ b/core/src/main/res/drawable/ic_add_image.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/ic_cancel.xml b/core/src/main/res/drawable/ic_cancel.xml new file mode 100644 index 000000000..d4c5752fc --- /dev/null +++ b/core/src/main/res/drawable/ic_cancel.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/ic_register.xml b/core/src/main/res/drawable/ic_register.xml new file mode 100644 index 000000000..e5c2b50a4 --- /dev/null +++ b/core/src/main/res/drawable/ic_register.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/koin_logo_gif.gif b/core/src/main/res/drawable/koin_logo_gif.gif new file mode 100644 index 000000000..dd0dc2fa2 Binary files /dev/null and b/core/src/main/res/drawable/koin_logo_gif.gif differ diff --git a/core/src/main/res/drawable/tooltip_share.gif b/core/src/main/res/drawable/tooltip_share.gif new file mode 100644 index 000000000..0a0345d37 Binary files /dev/null and b/core/src/main/res/drawable/tooltip_share.gif differ diff --git a/core/src/main/res/mipmap-hdpi/ic_launcher_koin_business.png b/core/src/main/res/mipmap-hdpi/ic_launcher_koin_business.png new file mode 100644 index 000000000..925e8381f Binary files /dev/null and b/core/src/main/res/mipmap-hdpi/ic_launcher_koin_business.png differ diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 9e851c73c..1159bc6a2 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -12,6 +12,8 @@ 확인하기 아니오 취소 + 취소하기 + 등록하기 완료 편집 저장 @@ -46,7 +48,14 @@ 닫기 내 상점 - + 사진 + 제목 + 시작일 + 종료일 + 필수 입력 항목입니다. + 삭제 + 사진 등록하기 + 이미지는 %d개까지만 등록 가능해요. @@ -90,6 +99,7 @@ \u0040 koreatech.ac.kr https://portal.koreatech.ac.kr/ + https://job.koreatech.ac.kr/ https://email.koreatech.ac.kr/ in.koreatech.koin.core.helpers.UserLockBottomSheetBehavior @@ -224,5 +234,20 @@ 알림설정 - + 이벤트/공지 작성하기 + 이벤트/공지와 관련된 사진을 올려보세요. + 이벤트/공지 제목을 입력해주세요. + 이벤트/공지 내용 + 이벤트/공지 내용을 입력해주세요. + 이벤트/공지 등록 기간 + + + 업데이트하기 + 이미 업데이트를 하셨나요? + 이미 업데이트를 하셨나요? + 업데이트 이후에도 이 화면이 나타나는\n경우에는 스토어에서 코인을\n삭제 후 재설치 해 주세요. + 스토어로 가기 + + + Copyright 2024. BCSD Lab. All rights reserved. diff --git a/core/src/main/res/values/text_appearance.xml b/core/src/main/res/values/text_appearance.xml index 2778b91eb..7e9a9b5dc 100644 --- a/core/src/main/res/values/text_appearance.xml +++ b/core/src/main/res/values/text_appearance.xml @@ -71,7 +71,7 @@