diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8aecc629..75afb311 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,13 +5,11 @@ - - - - + Unit, + modifier: Modifier = Modifier +) { + val backgroundColor = when { + enabled -> PawKeyTheme.colors.primary + else -> PawKeyTheme.colors.background1 + } + + val textColor = when { + enabled -> PawKeyTheme.colors.background1 + else -> PawKeyTheme.colors.default + } + + Box( + modifier = modifier + .fillMaxWidth() + .background(backgroundColor, shape = RoundedCornerShape(8.dp)) + .noRippleClickable { + if (enabled) onClick() + } + .padding(vertical = 14.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = PawKeyTheme.typography.mainButtonDefault, + color = textColor + ) + } +} + +@Preview +@Composable +private fun DogkyButtonPreview() { + PawKeyTheme { + DogkyButton( + text = "", + enabled = true, + onClick = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/PawKeyBottomSheet.kt b/app/src/main/java/com/paw/key/core/designsystem/component/PawKeyBottomSheet.kt new file mode 100644 index 00000000..fdc18dcb --- /dev/null +++ b/app/src/main/java/com/paw/key/core/designsystem/component/PawKeyBottomSheet.kt @@ -0,0 +1,50 @@ +package com.paw.key.core.designsystem.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PawKeyBottomSheet( + sheetState: SheetState, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable (sheetState: SheetState) -> Unit +) { + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + containerColor = PawKeyTheme.colors.background2, + modifier = modifier + .fillMaxWidth(), + dragHandle = null, + ) { + content(sheetState) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PawKeyBottomSheetPreview() { + PawKeyTheme { + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + + PawKeyBottomSheet( + sheetState = sheetState, + onDismissRequest = {} + ) { } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/core/designsystem/component/PawkeyButton.kt b/app/src/main/java/com/paw/key/core/designsystem/component/PawkeyButton.kt index aeb4181a..2c0fda0e 100644 --- a/app/src/main/java/com/paw/key/core/designsystem/component/PawkeyButton.kt +++ b/app/src/main/java/com/paw/key/core/designsystem/component/PawkeyButton.kt @@ -85,6 +85,7 @@ private fun PreviewPawkeyButton() { } } +// Todo : 이 더러운 분기의 버튼 제거예정 @Composable fun PawkeyButton( text: String, @@ -102,7 +103,7 @@ fun PawkeyButton( } val backgroundColor = when { - actualEnabled && !isBackGround -> PawKeyTheme.colors.green500 + actualEnabled && !isBackGround -> PawKeyTheme.colors.primary actualEnabled && isBackGround -> PawKeyTheme.colors.white1 !actualEnabled && isBackGround -> PawKeyTheme.colors.white1 else -> PawKeyTheme.colors.gray200 @@ -139,7 +140,7 @@ fun PawkeyButton( ) { Text( text = text, - style = PawKeyTheme.typography.body16Sb, + style = PawKeyTheme.typography.mainButtonActive, color = contentColor ) } diff --git a/app/src/main/java/com/paw/key/core/util/DateVisualTransformation.kt b/app/src/main/java/com/paw/key/core/util/DateVisualTransformation.kt new file mode 100644 index 00000000..188fc499 --- /dev/null +++ b/app/src/main/java/com/paw/key/core/util/DateVisualTransformation.kt @@ -0,0 +1,44 @@ +package com.paw.key.core.util + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation + +// 날짜 텍스트 입력받아서 변환 텍스트 만들기 +class DateVisualTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + val digitsOnly = text.text.filter { it.isDigit() } + + val trimmed = if (digitsOnly.length > 8) digitsOnly.substring(0, 8) else digitsOnly + + val formattedText = buildString { + trimmed.forEachIndexed { index, char -> + append(char) + if (index == 3 || index == 5) { + append('/') + } + } + } + + val offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return when { + offset >= 6 -> offset + 2 + offset >= 4 -> offset + 1 + else -> offset + } + } + + override fun transformedToOriginal(offset: Int): Int { + return when { + offset >= 8 -> offset - 2 // yyyy/MM/dd + offset >= 5 -> offset - 1 // yyyy/MM + else -> offset + } + } + } + + return TransformedText(AnnotatedString(formattedText), offsetMapping) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/util/PermissionRequestEffect.kt b/app/src/main/java/com/paw/key/core/util/PermissionRequestEffect.kt similarity index 93% rename from app/src/main/java/com/paw/key/presentation/ui/course/util/PermissionRequestEffect.kt rename to app/src/main/java/com/paw/key/core/util/PermissionRequestEffect.kt index 3e3c683e..e87302f0 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/util/PermissionRequestEffect.kt +++ b/app/src/main/java/com/paw/key/core/util/PermissionRequestEffect.kt @@ -1,4 +1,4 @@ -package com.paw.key.presentation.ui.course.util +package com.paw.key.core.util import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts diff --git a/app/src/main/java/com/paw/key/core/util/flattenCoordinatesToLatLng.kt b/app/src/main/java/com/paw/key/core/util/flattenCoordinatesToLatLng.kt new file mode 100644 index 00000000..37251be9 --- /dev/null +++ b/app/src/main/java/com/paw/key/core/util/flattenCoordinatesToLatLng.kt @@ -0,0 +1,15 @@ +package com.paw.key.core.util + +import com.naver.maps.geometry.LatLng +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +fun flattenCoordinatesToLatLng( + coordinates: List>>> +): ImmutableList> { + return coordinates.map { polygon -> // 각 Polygon + polygon.firstOrNull()?.map { point -> + LatLng(point.first, point.second) + }.orEmpty().toPersistentList() + }.toPersistentList() +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/core/util/saveBitmapToCache.kt b/app/src/main/java/com/paw/key/core/util/saveBitmapToCache.kt new file mode 100644 index 00000000..1333d8e5 --- /dev/null +++ b/app/src/main/java/com/paw/key/core/util/saveBitmapToCache.kt @@ -0,0 +1,56 @@ +package com.paw.key.core.util + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import androidx.core.content.FileProvider +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +// 혹시나 비트맵 사용 시 +fun saveBitmapToCache( + context: Context, + bitmap: Bitmap, + maxFiles: Int = 5, // 캐시 최대 개수 + maxCacheSizeBytes: Long = 50L * 1024 * 1024, // 50mb, + childName : String = "map_snapshot_" // 이미지 이름 변경용 +): Result { + val cacheDir = context.cacheDir + + // 기존 스냅샷 파일 목록 오래된 순 정렬 + val existingFiles = cacheDir.listFiles { file -> + file.name.startsWith(childName) && file.extension == "png" + }?.sortedBy { it.lastModified() } ?: emptyList() + + // 최대 개수 초과 시 오래된 파일 삭제 LRU 구현 + if (existingFiles.size >= maxFiles) { + val filesToDelete = existingFiles.take(existingFiles.size - maxFiles + 1) + filesToDelete.forEach { it.delete() } + } + + // 용량 기반 삭제 + var totalSize = existingFiles.sumOf { it.length() } + val iterator = existingFiles.iterator() + while (totalSize > maxCacheSizeBytes && iterator.hasNext()) { + val file = iterator.next() + totalSize -= file.length() + file.delete() + } + + val imageFile = File(cacheDir, "${childName}${System.currentTimeMillis()}.png") + return try { + FileOutputStream(imageFile).use { out -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) + } + Result.success( + FileProvider.getUriForFile( + context, + "${context.packageName}.fileprovider", + imageFile + ) + ) + } catch (e: IOException) { + Result.failure(e) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/di/AppModule.kt b/app/src/main/java/com/paw/key/data/di/AppModule.kt index f86f1af8..fcb2ad21 100644 --- a/app/src/main/java/com/paw/key/data/di/AppModule.kt +++ b/app/src/main/java/com/paw/key/data/di/AppModule.kt @@ -1,11 +1,15 @@ package com.paw.key.data.di +import android.content.ContentResolver +import android.content.Context import com.paw.key.BuildConfig import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Named +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) @@ -16,4 +20,10 @@ object AppModule { fun provideKakaoNativeKey(): String { return BuildConfig.KAKAO_NATIVE_KEY } + + @Provides + @Singleton + fun provideContentResolver(@ApplicationContext context: Context): ContentResolver { + return context.contentResolver + } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingRegionResponse.kt b/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingRegionResponse.kt index 4bb07413..a7aef12a 100644 --- a/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingRegionResponse.kt +++ b/app/src/main/java/com/paw/key/data/dto/response/onboarding/OnboardingRegionResponse.kt @@ -1,7 +1,3 @@ -import com.paw.key.domain.model.entity.onboarding.District -import com.paw.key.domain.model.entity.onboarding.Dong -import com.paw.key.domain.model.entity.onboarding.Gu -import com.paw.key.domain.model.entity.onboarding.OnboardingRegion import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -48,31 +44,4 @@ data class DongDto( @SerialName("name") val name: String -) - -fun DistrictResponse.toDomain(): OnboardingRegion { - return OnboardingRegion( - districtList = this.data.districtDtos.map { it.toDomain() } - ) -} - -fun DistrictDto.toDomain(): District { - return District( - gu = this.gu.toDomain(), - dongs = this.dongs.map { it.toDomain() } - ) -} - -fun GuDto.toDomain(): Gu { - return Gu( - id = this.id, - name = this.name - ) -} - -fun DongDto.toDomain(): Dong { - return Dong( - id = this.id, - name = this.name - ) -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/remote/datasource/RegionDataSource.kt b/app/src/main/java/com/paw/key/data/remote/datasource/RegionDataSource.kt index fa39eae0..d3be31e8 100644 --- a/app/src/main/java/com/paw/key/data/remote/datasource/RegionDataSource.kt +++ b/app/src/main/java/com/paw/key/data/remote/datasource/RegionDataSource.kt @@ -7,4 +7,6 @@ class RegionDataSource @Inject constructor ( private val regionService: RegionService ) { suspend fun getRegionGeometry(userId: Int, regionId: Int) = regionService.getRegionGeometry(userId, regionId) + + suspend fun getRegionsList() = regionService.getRegionsList() } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/repositoryimpl/RegionRepositoryImpl.kt b/app/src/main/java/com/paw/key/data/repositoryimpl/RegionRepositoryImpl.kt index 42d82910..8c435d2b 100644 --- a/app/src/main/java/com/paw/key/data/repositoryimpl/RegionRepositoryImpl.kt +++ b/app/src/main/java/com/paw/key/data/repositoryimpl/RegionRepositoryImpl.kt @@ -3,6 +3,8 @@ package com.paw.key.data.repositoryimpl import com.paw.key.data.mapper.RegionMapper import com.paw.key.data.remote.datasource.RegionDataSource import com.paw.key.domain.model.entity.region.RegionDataEntity +import com.paw.key.domain.model.entity.signup.DistrictEntity +import com.paw.key.domain.model.entity.signup.toEntity import com.paw.key.domain.repository.RegionRepository import javax.inject.Inject @@ -15,4 +17,8 @@ class RegionRepositoryImpl @Inject constructor( mapper.mapDtoToEntity(it) } } + + override suspend fun getRegionList(): Result> = runCatching { + regionDataSource.getRegionsList().data.districtDtos.map { it.toEntity() } + } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/data/service/RegionService.kt b/app/src/main/java/com/paw/key/data/service/RegionService.kt index e262de63..ad3e1d3c 100644 --- a/app/src/main/java/com/paw/key/data/service/RegionService.kt +++ b/app/src/main/java/com/paw/key/data/service/RegionService.kt @@ -1,5 +1,6 @@ package com.paw.key.data.service +import DistrictDataDto import com.paw.key.data.dto.response.BaseResponse import com.paw.key.data.dto.response.region.RegionResponseDto import retrofit2.http.GET @@ -12,4 +13,7 @@ interface RegionService { @Header("X-USER-ID") userId: Int, @Path("regionId") regionId: Int, ): BaseResponse + + @GET("regions") + suspend fun getRegionsList(): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/model/entity/onboarding/OnboardingEntity.kt b/app/src/main/java/com/paw/key/domain/model/entity/onboarding/OnboardingEntity.kt index e7046ab4..8cf30fc5 100644 --- a/app/src/main/java/com/paw/key/domain/model/entity/onboarding/OnboardingEntity.kt +++ b/app/src/main/java/com/paw/key/domain/model/entity/onboarding/OnboardingEntity.kt @@ -1,5 +1,7 @@ package com.paw.key.domain.model.entity.onboarding +import com.paw.key.domain.model.entity.signup.DistrictEntity + data class OnboardingInfo( val userId: Int, val userName: String, @@ -24,22 +26,7 @@ data class PetTraitCategoryOption( ) data class OnboardingRegion( - val districtList: List -) - -data class District( - val gu: Gu, - val dongs: List -) - -data class Gu( - val id: Int, - val name: String -) - -data class Dong( - val id: Int, - val name: String + val districtList: List ) data class PetTraitDto( diff --git a/app/src/main/java/com/paw/key/domain/model/entity/signup/DistrictEntity.kt b/app/src/main/java/com/paw/key/domain/model/entity/signup/DistrictEntity.kt new file mode 100644 index 00000000..85833a26 --- /dev/null +++ b/app/src/main/java/com/paw/key/domain/model/entity/signup/DistrictEntity.kt @@ -0,0 +1,41 @@ +package com.paw.key.domain.model.entity.signup + +import DistrictDto +import DongDto +import GuDto + +data class DistrictEntity( + val gu: Gu, + val dongs: List +) + +data class Gu( + val id: Int, + val name: String +) + +data class Dong( + val id: Int, + val name: String +) + +fun DistrictDto.toEntity(): DistrictEntity { + return DistrictEntity( + gu = gu.toEntity(), + dongs = dongs.map { it.toEntity() } + ) +} + +fun GuDto.toEntity(): Gu { + return Gu( + id = id, + name = name + ) +} + +fun DongDto.toEntity(): Dong { + return Dong( + id = id, + name = name + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/domain/repository/RegionRepository.kt b/app/src/main/java/com/paw/key/domain/repository/RegionRepository.kt index 05a06930..b74830df 100644 --- a/app/src/main/java/com/paw/key/domain/repository/RegionRepository.kt +++ b/app/src/main/java/com/paw/key/domain/repository/RegionRepository.kt @@ -1,7 +1,9 @@ package com.paw.key.domain.repository import com.paw.key.domain.model.entity.region.RegionDataEntity +import com.paw.key.domain.model.entity.signup.DistrictEntity interface RegionRepository { suspend fun getRegionGeometry(userId: Int, regionId: Int): Result + suspend fun getRegionList(): Result> } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/community/CommunityScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/community/CommunityScreen.kt index 57c1f4f5..060ec9f6 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/community/CommunityScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/community/CommunityScreen.kt @@ -4,7 +4,9 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -14,6 +16,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.paw.key.R import com.paw.key.core.designsystem.theme.PawKeyTheme @@ -42,26 +45,37 @@ fun CommunityScreen( snackBarHostState: SnackbarHostState, modifier: Modifier = Modifier, ) { - Column ( + Column( modifier = modifier .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center - ){ + ) { Image( imageVector = ImageVector.vectorResource(R.drawable.ic_community), - contentDescription = stringResource(R.string.ic_community_description), + contentDescription = stringResource(R.string.ic_community_description) ) - Text( - text = "커뮤니티 기능은\n 아직 준비중이에요", - modifier = modifier, - style = PawKeyTheme.typography.body16Sb, - color = PawKeyTheme.colors.gray300 - ) + Spacer(modifier = Modifier.height(4.dp)) + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "커뮤니티 기능은", + style = PawKeyTheme.typography.body16Sb, + color = PawKeyTheme.colors.gray300 + ) + Text( + text = "아직 준비중이에요", + style = PawKeyTheme.typography.body16Sb, + color = PawKeyTheme.colors.gray300 + ) + } } } + @Preview @Composable private fun CommunityScreenPreview() { diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt index 7510e675..fc5954dd 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/entire/tab/map/List/CourseOptionBottomSheet.kt @@ -10,7 +10,6 @@ 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.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt index ef198e8a..5117cda1 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walk/WalkCourseScreen.kt @@ -78,7 +78,7 @@ import com.paw.key.core.util.PreferenceDataStore import com.paw.key.core.util.UiState import com.paw.key.core.extension.noRippleClickable import com.paw.key.presentation.ui.course.util.FusedLocationSource -import com.paw.key.presentation.ui.course.util.PermissionRequestEffect +import com.paw.key.core.util.PermissionRequestEffect import com.paw.key.presentation.ui.course.util.StepCountListener import com.paw.key.presentation.ui.course.util.rememberCustomFusedLocationSource import com.paw.key.presentation.ui.course.util.rememberStepCounter diff --git a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt index 01ac302a..3af5fb97 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/course/walkreview/WalkReviewScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -28,6 +27,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext @@ -39,11 +42,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import coil.compose.AsyncImage import com.paw.key.R +import com.paw.key.core.designsystem.component.DataLoadingScreen import com.paw.key.core.designsystem.component.PawkeyButton import com.paw.key.core.designsystem.component.SubChip import com.paw.key.core.designsystem.component.TopBar import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewDialog import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewFeedbackForm import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewFeedbackHeader import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewImageRow @@ -51,6 +54,8 @@ import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewInfoHol import com.paw.key.presentation.ui.course.walkreview.component.WalkReviewTextField import com.paw.key.presentation.ui.course.walkreview.state.WalkReviewContract import com.paw.key.presentation.ui.course.walkreview.viewmodel.WalkReviewViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @Composable @@ -67,6 +72,8 @@ fun WalkReviewRoute( val state by viewModel.state.collectAsStateWithLifecycle() val isValid = state.isValidForm val context = LocalContext.current + var isLoading by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() val lifecycleOwner = LocalLifecycleOwner.current @@ -161,16 +168,25 @@ fun WalkReviewRoute( viewModel.onImageDelete(it) }, onClickPublic = { isShare -> - viewModel.postWalkReview( - routeId = routeId, - isShare = isShare - ) + coroutineScope.launch { + isLoading = false + delay(3000L) + isLoading = true + viewModel.postWalkReview( + routeId = routeId, + isShare = isShare + ) + } }, /*navigateShared = { navigateShared(routeId) },*/ modifier = modifier, ) + + if (isLoading) { + DataLoadingScreen() + } } @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/component/DaytimeCard.kt b/app/src/main/java/com/paw/key/presentation/ui/home/component/DaytimeCard.kt index f42fcbce..d0c6c52f 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/component/DaytimeCard.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/component/DaytimeCard.kt @@ -43,7 +43,7 @@ fun DaytimeCard( Box( contentAlignment = Alignment.BottomCenter, modifier = modifier - .width(81.dp) + .fillMaxWidth() .height(110.dp) .background( color = PawKeyTheme.colors.white1, @@ -57,7 +57,7 @@ fun DaytimeCard( .fillMaxWidth() .padding(horizontal = 11.dp) .padding(bottom = 14.dp) - .zIndex(1F), + .zIndex(2F), ) { Text( text = daytime, diff --git a/app/src/main/java/com/paw/key/presentation/ui/home/component/TrackingCard.kt b/app/src/main/java/com/paw/key/presentation/ui/home/component/TrackingCard.kt index df2cbca6..cfcfc023 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/home/component/TrackingCard.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/home/component/TrackingCard.kt @@ -42,8 +42,8 @@ fun TrackingCard( ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier - .width(235.dp) + modifier = modifier + .fillMaxWidth() .height(110.dp) .background( color = PawKeyTheme.colors.black, diff --git a/app/src/main/java/com/paw/key/presentation/ui/main/MainNavigator.kt b/app/src/main/java/com/paw/key/presentation/ui/main/MainNavigator.kt index bb784688..09fdbe64 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/main/MainNavigator.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/main/MainNavigator.kt @@ -30,7 +30,7 @@ import com.paw.key.presentation.ui.mypage.navigation.navigateSavedDetail import com.paw.key.presentation.ui.mypage.navigation.navigateUserProfile import com.paw.key.presentation.ui.onboard.navigation.navigateOnboarding import com.paw.key.presentation.ui.region.navigation.navigateRegional -import com.paw.key.presentation.ui.signup.navigation.navigateSignUpFlow +import com.paw.key.presentation.ui.signup.navigation.navigateSignUp import com.paw.key.presentation.ui.splash.navigation.Splash class MainNavigator( @@ -83,11 +83,6 @@ class MainNavigator( navController.navigateLogin(navOptions = navOptions) } - - fun navigateSignUp(navOptions: NavOptions? = null) { - navController.navigateSignUpFlow(navOptions) - } - fun navigateMyPage(navOptions: NavOptions? = null) { navController.navigateMyPage(navOptions = navOptions) } @@ -117,8 +112,9 @@ class MainNavigator( ) } - fun navigateSignUpFlow(navOptions: NavOptions? = null) { - navController.navigateSignUpFlow(navOptions) + // Todo : 나중에 로직 플로우 확인하고 수정예정 + fun navigateSignUp(navOptions: NavOptions? = null) { + navController.navigateSignUp(navOptions) } fun navigateArchivedCourse(navOptions: NavOptions? = null) { diff --git a/app/src/main/java/com/paw/key/presentation/ui/main/PawKeyNavHost.kt b/app/src/main/java/com/paw/key/presentation/ui/main/PawKeyNavHost.kt index a39e1e39..1b9ec1db 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/main/PawKeyNavHost.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/main/PawKeyNavHost.kt @@ -1,10 +1,10 @@ package com.paw.key.presentation.ui.main import android.os.Build -import android.util.Log import androidx.annotation.RequiresApi -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -19,7 +19,6 @@ import com.paw.key.presentation.ui.course.sharedwalk.review.navigation.sharedWal import com.paw.key.presentation.ui.course.sharedwalk.sharedroute.navigation.sharedWalkCourseNavGraph import com.paw.key.presentation.ui.course.walk.navigation.walkCourseNavGraph import com.paw.key.presentation.ui.course.walkcomplete.navigation.walkCompletionNavGraph -import com.paw.key.presentation.ui.course.walkreview.navigation.navigateWalkReview import com.paw.key.presentation.ui.course.walkreview.navigation.walkReviewNavGraph import com.paw.key.presentation.ui.dummy.navigation.dummyNavGraph import com.paw.key.presentation.ui.dummy.next.dummyNextNavGraph @@ -49,10 +48,30 @@ fun PawKeyNavHost( NavHost( navController = navigator.navController, startDestination = navigator.startDestination, - enterTransition = { EnterTransition.None }, - exitTransition = { ExitTransition.None }, - popEnterTransition = { EnterTransition.None }, - popExitTransition = { ExitTransition.None }, + enterTransition = { + slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(durationMillis = 300) + ) + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth }, + animationSpec = tween(durationMillis = 300) + ) + }, + popEnterTransition = { + slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth }, + animationSpec = tween(durationMillis = 300) + ) + }, + popExitTransition = { + slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = tween(durationMillis = 300) + ) + }, ) { homeNavGraph( paddingValues = paddingValues, @@ -258,7 +277,7 @@ fun PawKeyNavHost( onboardingNavGraph( paddingValues = paddingValues, navigateUp = navigator::navigateUp, - navigateNext = navigator::navigateDummyNext, + navigateNext = navigator::navigateSignUp, navigateSignUp = navigator::navigateLogin, snackBarHostState = snackbarHostState ) @@ -270,7 +289,7 @@ fun PawKeyNavHost( navigator.navigateUp() }, navigateNext = { - navigator.navigateSignUpFlow() + //navigator.navigateSignUpFlow() }, navigateHome = { navigator.navigateHome() @@ -286,7 +305,7 @@ fun PawKeyNavHost( ) signUpNavGraph( - navController = navigator.navController, + navigateUp = navigator::navigateUp, navigateToHome = { val options = navOptions { popUpTo(0) { inclusive = true } @@ -295,7 +314,5 @@ fun PawKeyNavHost( navigator.navigateHome(options) } ) - - } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/mypage/navigation/ArchivedDetailNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/mypage/navigation/ArchivedDetailNavigation.kt index a29766cc..db2eee7a 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/mypage/navigation/ArchivedDetailNavigation.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/mypage/navigation/ArchivedDetailNavigation.kt @@ -8,7 +8,6 @@ import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.paw.key.core.navigation.Route import com.paw.key.presentation.ui.mypage.ArchivedDetailRoute -import com.paw.key.presentation.ui.region.navigation.Regional import kotlinx.serialization.Serializable fun NavController.navigateArchivedDetail( @@ -21,6 +20,7 @@ fun NavController.navigateArchivedDetail( fun NavGraphBuilder.archivedDetailNavGraph( navigateUp: () -> Unit, + //navigateDetail: () -> Unit, navigateToSharedWalk: (Int, Int) -> Unit, modifier: Modifier = Modifier, ) { @@ -32,6 +32,7 @@ fun NavGraphBuilder.archivedDetailNavGraph( navigateToSharedWalk = { routeId, pageId -> navigateToSharedWalk(routeId, pageId) }, + //navigateDetail = navigateDetail, routeId = archivedDetail.routeId, pageId = archivedDetail.pageId, modifier = modifier diff --git a/app/src/main/java/com/paw/key/presentation/ui/onboard/OnboardingScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/onboard/OnboardingScreen.kt index cedb33bd..f3627097 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/onboard/OnboardingScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/onboard/OnboardingScreen.kt @@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -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.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -22,6 +20,21 @@ import com.paw.key.core.designsystem.theme.PawKeyTheme import com.paw.key.presentation.ui.onboard.component.OnboardPager import com.paw.key.presentation.ui.onboard.component.OnboardingPosting +@Preview(showBackground = true) +@Composable +private fun PreviewOnboardingScreen() { + PawKeyTheme { + OnboardingScreen( + paddingValues = PaddingValues(), + navigateUp = {}, + navigateNext = {}, + navigateSignUp = {}, + snackBarHostState = SnackbarHostState(), + modifier = Modifier + ) + } +} + @Composable fun OnboardingRoute( paddingValues: PaddingValues, @@ -43,21 +56,6 @@ fun OnboardingRoute( ) } -@Preview(showBackground = true) -@Composable -fun PreviewOnboardingScreen() { - PawKeyTheme { - OnboardingScreen( - paddingValues = PaddingValues(), - navigateUp = {}, - navigateNext = {}, - navigateSignUp = {}, - snackBarHostState = SnackbarHostState(), - modifier = Modifier - ) - } -} - @Composable fun OnboardingScreen( paddingValues: PaddingValues, @@ -110,7 +108,7 @@ fun OnboardingScreen( PawkeyButton( text = "신규 계정으로 회원가입", enabled = true, - onClick = { }, + onClick = navigateNext, // Todo : 나중에 로그인, 회원가입 네이밍 수정 isBackGround = true, isBorder = false ) diff --git a/app/src/main/java/com/paw/key/presentation/ui/region/RegionalManagementScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/region/RegionalManagementScreen.kt index 71f0626f..6384ab3f 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/region/RegionalManagementScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/region/RegionalManagementScreen.kt @@ -24,11 +24,9 @@ import androidx.compose.runtime.remember 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.layout.onSizeChanged -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner @@ -58,8 +56,8 @@ fun RegionalManagementRoute( snackBarHostState: SnackbarHostState, navigateUp: () -> Unit, navigateNext: () -> Unit, - regionId: Int, modifier: Modifier = Modifier, + regionId: Int? = -1, viewModel: RegionViewModel = hiltViewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -108,6 +106,7 @@ fun RegionalManagementRoute( paddingValues = paddingValues, snackBarHostState = snackBarHostState, cameraPositionState = cameraPositionState, + regionId = regionId, type = state.drawType, regionCoordinates = uiState.data, selectedRegion = state.selectedRegion, @@ -139,6 +138,7 @@ fun RegionalManagementScreen( cameraPositionState: CameraPositionState, type: DrawType, regionCoordinates: ImmutableList>, + regionId: Int?, selectedRegion: String?, preRegionName: String?, regionName: String?, @@ -152,7 +152,7 @@ fun RegionalManagementScreen( SnackbarHost( hostState = snackBarHostState, modifier = Modifier.padding( - bottom = LocalConfiguration.current.screenHeightDp.dp * 0.4f + bottom = LocalWindowInfo.current.containerSize.height.dp * 0.4f ) ) { data -> CustomSnackBar( @@ -177,7 +177,7 @@ fun RegionalManagementScreen( if (singlePolygonCoords.isNotEmpty()) { PolygonOverlay( coords = singlePolygonCoords, - color = PawKeyTheme.colors.green500.copy(alpha = 0.3f), + color = PawKeyTheme.colors.opacityPrimary.copy(alpha = 0.3f), outlineWidth = 1.dp, outlineColor = PawKeyTheme.colors.green500 ) @@ -188,7 +188,7 @@ fun RegionalManagementScreen( regionCoordinates.forEach { PolygonOverlay( coords = it, - color = PawKeyTheme.colors.green500.copy(alpha = 0.3f), + color = PawKeyTheme.colors.opacityPrimary.copy(alpha = 0.3f), outlineWidth = 1.dp, outlineColor = PawKeyTheme.colors.green500 ) @@ -222,122 +222,66 @@ fun RegionalManagementScreen( ) { Text( text = "선택한 위치", - style = PawKeyTheme.typography.head20B2, - color = PawKeyTheme.colors.black + style = PawKeyTheme.typography.header3, + color = PawKeyTheme.colors.contents ) Text( text = regionName ?: "강남구 역삼동", - style = PawKeyTheme.typography.head20B2, - color = PawKeyTheme.colors.green500 + style = PawKeyTheme.typography.header3, + color = PawKeyTheme.colors.primary ) } Spacer(modifier = Modifier.height(12.dp)) - if (regionName == preRegionName) { - Text( - text = "기존에 산책하던 지역은\n" + - "기존 지역과 같은 동네에요.", - style = PawKeyTheme.typography.body14M, - color = PawKeyTheme.colors.gray500, - modifier = Modifier - .padding(bottom = 12.dp) - ) - - Spacer(modifier = Modifier.height(12.dp)) - - PawkeyButton( - text = "지역 변경하기", - onClick = { - onClickButton() - }, - modifier = Modifier - .fillMaxWidth(), - enabled = false, - ) - } else { - Text( - text = "기존에 산책하던 지역은 ${preRegionName}이에요.\n선택한 위치로 산책 지역을 변경하시겠어요?", - style = PawKeyTheme.typography.body14M, - color = PawKeyTheme.colors.gray500, - modifier = Modifier - .padding(bottom = 12.dp) - ) - - Spacer(modifier = Modifier.height(12.dp)) - - PawkeyButton( - text = "지역 변경하기", - onClick = { - onClickButton() - }, - modifier = Modifier - .fillMaxWidth(), - enabled = true, - ) - } - } - } - } -} + if (regionId == -1) { + if (regionName == preRegionName) { + Text( + text = "기존에 산책하던 지역은\n" + + "기존 지역과 같은 동네에요.", + style = PawKeyTheme.typography.bodyDefault, + color = PawKeyTheme.colors.gray500, + modifier = Modifier + .padding(bottom = 12.dp) + ) -@Preview(showBackground = true) -@Composable -private fun RadiusTestPreview() { - PawKeyTheme { - Scaffold( - modifier = Modifier - .fillMaxSize() - .background(Color.Blue) - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.Blue) - .padding(paddingValues) - ) { - Box( - modifier = Modifier - .weight(1f), - ) + Spacer(modifier = Modifier.height(12.dp)) - Column( - modifier = Modifier - .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) - .background( - color = PawKeyTheme.colors.black, - shape = RoundedCornerShape( - topStart = 16.dp, topEnd = 16.dp - ) + PawkeyButton( + text = "지역 변경하기", + onClick = { + onClickButton() + }, + modifier = Modifier + .fillMaxWidth(), + enabled = false, ) - .padding(horizontal = 16.dp, vertical = 24.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp, bottom = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { + } else { Text( - text = "선택한 위치", - style = PawKeyTheme.typography.head20B2, - color = PawKeyTheme.colors.black + text = "기존에 산책하던 지역은 ${preRegionName}이에요.\n선택한 위치로 산책 지역을 변경하시겠어요?", + style = PawKeyTheme.typography.bodyDefault, + color = PawKeyTheme.colors.gray500, + modifier = Modifier + .padding(bottom = 12.dp) ) - Text( - text = "강남구 역삼동", - style = PawKeyTheme.typography.head20B2, - color = PawKeyTheme.colors.green500 + Spacer(modifier = Modifier.height(12.dp)) + + PawkeyButton( + text = "지역 변경하기", + onClick = { + onClickButton() + }, + modifier = Modifier + .fillMaxWidth(), + enabled = true, ) } - - Spacer(modifier = Modifier.height(12.dp)) - + } else { Text( - text = "기존에 산책하던 지역은이에요.\n선택한 위치로 산책 지역을 변경하시겠어요?", - style = PawKeyTheme.typography.body14M, + text = "선택한 산책 지역은 ${regionName}이에요.\n이 위치로 산책 지역을 설정하시겠어요?", + style = PawKeyTheme.typography.bodyDefault, color = PawKeyTheme.colors.gray500, modifier = Modifier .padding(bottom = 12.dp) @@ -346,9 +290,9 @@ private fun RadiusTestPreview() { Spacer(modifier = Modifier.height(12.dp)) PawkeyButton( - text = "지역 변경하기", + text = "선택", onClick = { - + onClickButton() }, modifier = Modifier .fillMaxWidth(), @@ -358,4 +302,4 @@ private fun RadiusTestPreview() { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/region/navigation/RegionalNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/region/navigation/RegionalNavigation.kt index 101ea8f6..ae89a818 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/region/navigation/RegionalNavigation.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/region/navigation/RegionalNavigation.kt @@ -7,7 +7,6 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import androidx.navigation.toRoute import com.paw.key.core.navigation.Route import com.paw.key.presentation.ui.region.RegionalManagementRoute import kotlinx.serialization.Serializable @@ -26,14 +25,12 @@ fun NavGraphBuilder.regionalNavGraph( snackBarHostState: SnackbarHostState, modifier: Modifier = Modifier, ) { - composable {backStackEntry -> - val regional = backStackEntry.toRoute() + composable { RegionalManagementRoute( paddingValues = paddingValues, snackBarHostState = snackBarHostState, navigateUp = navigateUp, navigateNext = navigateNext, - regionId = regional.regionId, modifier = modifier ) } @@ -41,5 +38,5 @@ fun NavGraphBuilder.regionalNavGraph( @Serializable data class Regional( - val regionId: Int + val regionId: Int? = -1 ) : Route \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt index e12a3dc3..331ec7d1 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/region/viewmodel/RegionViewModel.kt @@ -5,9 +5,9 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute -import com.naver.maps.geometry.LatLng import com.paw.key.core.util.PreferenceDataStore import com.paw.key.core.util.UiState +import com.paw.key.core.util.flattenCoordinatesToLatLng import com.paw.key.core.util.handleError import com.paw.key.domain.repository.RegionRepository import com.paw.key.domain.repository.home.HomeRegionRepository @@ -16,7 +16,6 @@ import com.paw.key.presentation.ui.region.state.DrawType import com.paw.key.presentation.ui.region.state.RegionSideEffect import com.paw.key.presentation.ui.region.state.RegionState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -28,6 +27,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -53,18 +53,28 @@ class RegionViewModel @Inject constructor( ) init { - viewModelScope.launch { - Log.e("RegionViewModel", "regionId: ${regionIdState.regionId}") - val validUserId = userId.filter { it != -1 }.first() - getRegionGeometry( - userId = validUserId, - regionId = regionIdState.regionId, - ) + if (regionIdState.regionId != -1) { + viewModelScope.launch { + val validUserId = userId.filter { it != -1 }.first() + getRegionGeometry( + userId = validUserId, + regionId = regionIdState.regionId, + ) + } + } else { + viewModelScope.launch { + Timber.e("RegionViewModel test용 regionId: ${regionIdState.regionId}") + val validUserId = userId.filter { it != -1 }.first() + getRegionGeometry( + userId = validUserId, + regionId = 39, + ) + } } } - fun getRegionGeometry(userId: Int, regionId: Int) = viewModelScope.launch { - regionRepository.getRegionGeometry(userId, regionId) + fun getRegionGeometry(userId: Int, regionId: Int?) = viewModelScope.launch { + regionRepository.getRegionGeometry(userId, regionId!!) .onSuccess { data -> val coordinates = data.geometry.coordinates val flattenedLatLng = flattenCoordinatesToLatLng(coordinates) @@ -103,7 +113,6 @@ class RegionViewModel @Inject constructor( } } .onFailure { throwable -> - Log.e("RegionViewModel", "API 호출 실패", throwable) val errorMessage = handleError(throwable) _state.update { it.copy( @@ -114,21 +123,23 @@ class RegionViewModel @Inject constructor( } fun patchRegion() { - viewModelScope.launch { - homeRepository.patchRegion(userId.value, regionIdState.regionId) - .onSuccess { data -> - Log.d("RegionViewModel", "API 응답 성공: $data") - _sideEffect.emit( - RegionSideEffect.ShowSnackBar("지역을 ${state.value.regionName ?: "역삼동"}으로 변경했어요.") - ) - } - .onFailure { throwable -> - Log.e("RegionViewModel", "API 호출 실패", throwable) - val errorMessage = handleError(throwable) - _sideEffect.emit( - RegionSideEffect.ShowSnackBar(errorMessage) - ) - } + if (regionIdState.regionId != -1) { + viewModelScope.launch { + homeRepository.patchRegion(userId.value, regionIdState.regionId!!) + .onSuccess { data -> + Log.d("RegionViewModel", "API 응답 성공: $data") + _sideEffect.emit( + RegionSideEffect.ShowSnackBar("지역을 ${state.value.regionName ?: "역삼동"}으로 변경했어요.") + ) + } + .onFailure { throwable -> + Log.e("RegionViewModel", "API 호출 실패", throwable) + val errorMessage = handleError(throwable) + _sideEffect.emit( + RegionSideEffect.ShowSnackBar(errorMessage) + ) + } + } } } @@ -154,12 +165,3 @@ private fun flattenCoordinatesToLatLng( } */ -private fun flattenCoordinatesToLatLng( - coordinates: List>>> -): ImmutableList> { - return coordinates.map { polygon -> // 각 Polygon - polygon.firstOrNull()?.map { point -> - LatLng(point.first, point.second) - }.orEmpty().toPersistentList() - }.toPersistentList() -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpActivityScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpActivityScreen.kt deleted file mode 100644 index 6cf8d740..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpActivityScreen.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.paw.key.presentation.ui.signup - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.paw.key.R -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.presentation.ui.signup.component.FormField -import com.paw.key.presentation.ui.signup.component.LocationItem -import com.paw.key.presentation.ui.signup.component.LocationItemList -import com.paw.key.presentation.ui.signup.component.LocationList -import com.paw.key.presentation.ui.signup.component.SignUpHeader -import com.paw.key.presentation.ui.signup.viewmodel.SignUpViewModel - -@Preview(showBackground = true) -@Composable -private fun PreviewSignUpActivityScreen() { - PawKeyTheme { - SignUpActivityScreen( - step = 0.5F, - navigateSignUpDog = {}, - viewModel = hiltViewModel() - ) - } -} - -@Composable -fun SignUpActivityRoute( - navigateSignUpDog: () -> Unit, - modifier: Modifier = Modifier, - viewModel: SignUpViewModel? = null -) { - - SignUpActivityScreen( - step = 0.5F, - navigateSignUpDog = navigateSignUpDog, - modifier = modifier, - viewModel = viewModel ?: hiltViewModel() - ) -} - -@Composable -fun SignUpActivityScreen( - step: Float, - navigateSignUpDog: () -> Unit, - modifier: Modifier = Modifier, - viewModel: SignUpViewModel -) { - val state by viewModel.state.collectAsStateWithLifecycle() - val regionList by viewModel.regionList.collectAsStateWithLifecycle() - - val selectedGu = state.selectedGu - val selectedDong = state.selectedDong - - val guOptions = regionList.map { it.gu.name } - - val dongOptions = if (selectedGu.isNotEmpty()) { - regionList.find { it.gu.name == selectedGu }?.dongs?.map { - LocationItem(id = it.id, name = it.name) - } ?: emptyList() - } else { - emptyList() - } - - Column( - modifier = modifier.fillMaxSize() - ) { - SignUpHeader( - title = stringResource(R.string.ic_onboarding_signup), - subtitle = stringResource(id = R.string.ic_onboarding_signup_subtitle_step2), - progress = step, - ) - - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) - ) { - Spacer(modifier = Modifier.height(27.dp)) - - // 지역구 섹션 - 처음부터 모든 구 칩들을 보여줌 - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_main_location), - content = { - LocationList( - selected = selectedGu, - locations = guOptions, - onLocationSelected = { guName -> - val selectedGuItem = regionList.find { it.gu.name == guName } - selectedGuItem?.let { - viewModel.onGuSelected(it.gu.name, it.gu.id) - } - } - ) - } - ) - - Spacer(modifier = Modifier.height(46.dp)) - - if (selectedGu.isNotEmpty()) { - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_sub_location), - content = { - LocationItemList( - selected = selectedDong, - locations = dongOptions, - onLocationSelected = { locationItem -> - viewModel.onDongSelected(locationItem.name, locationItem.id) - } - ) - } - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - val isFormValid = selectedGu.isNotEmpty() && selectedDong.isNotEmpty() - - PawkeyButton( - text = stringResource(id = R.string.ic_onboarding_signup_button), - enabled = isFormValid, - onClick = { - if (isFormValid) { - navigateSignUpDog() - } - } - ) - - Spacer(modifier = Modifier.height(46.dp)) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt deleted file mode 100644 index 92a0b3a4..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpDogScreen.kt +++ /dev/null @@ -1,527 +0,0 @@ -package com.paw.key.presentation.ui.signup - -import android.Manifest -import android.net.Uri -import android.os.Build -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.RequiresApi -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -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.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Icon -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import coil.compose.AsyncImage -import com.paw.key.R -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.core.extension.noRippleClickable -import com.paw.key.presentation.ui.signup.component.FormField -import com.paw.key.presentation.ui.signup.component.SignUpTextField -import com.paw.key.presentation.ui.signup.component.SignUpUserSelectButton -import com.paw.key.presentation.ui.signup.state.SignUpContract -import com.paw.key.presentation.ui.signup.viewmodel.SignUpViewModel - -@Preview(showBackground = true) -@Composable -private fun PreviewSignUpDogScreen() { - PawKeyTheme { - // Preview content - } -} - -@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) -@Composable -fun SignUpDogRoute( - navigateNext: () -> Unit, - modifier: Modifier = Modifier, - viewModel: SignUpViewModel? = null -) { - val actualViewModel = viewModel ?: hiltViewModel() - SignUpDogScreen( - progress = 0.75F, - navigateNext = navigateNext, - modifier = modifier, - viewModel = actualViewModel - ) -} - -private fun isAgeValid(ageKnown: SignUpContract.AgeKnown, dogAge: String): Boolean { - return when (ageKnown) { - SignUpContract.AgeKnown.KNOWN -> dogAge.isNotEmpty() - SignUpContract.AgeKnown.UNKNOWN -> true - SignUpContract.AgeKnown.NONE -> false - } -} - -@RequiresApi(Build.VERSION_CODES.TIRAMISU) -@Composable -fun SignUpDogScreen( - navigateNext: () -> Unit, - modifier: Modifier = Modifier, - progress: Float = 1F, - viewModel: SignUpViewModel -) { - val state by viewModel.state.collectAsState() - val keyboardController = LocalSoftwareKeyboardController.current - val focusManager = LocalFocusManager.current - - val dogNameFocusRequester = remember { FocusRequester() } - val dogBreedFocusRequester = remember { FocusRequester() } - val dogAgeFocusRequester = remember { FocusRequester() } - - val animatedProgress by animateFloatAsState( - targetValue = progress, - animationSpec = tween( - durationMillis = 1000, - easing = FastOutSlowInEasing - ), - label = "progress_animation" - ) - - val imagePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - - val pickSingleMediaLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.PickVisualMedia() - ) { uri -> - if (uri != null) { - viewModel.onDogImageSelected(uri) - } - } - - val galleryLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.GetContent() - ) { uri: Uri? -> - if (uri != null) { - viewModel.onDogImageSelected(uri) - } - } - - val permissionLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted -> - if (isGranted) { - galleryLauncher.launch("image/*") - } - } - - val onClickImage = { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - pickSingleMediaLauncher.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) - ) - } else { - permissionLauncher.launch(imagePermission) - } - } - - val isFormValid = remember(state.dogName, state.dogGender, state.dogBreed, state.ageKnown, state.dogAge) { - state.dogName.isNotEmpty() && - state.dogGender != SignUpContract.DogGender.UNKNOWN && - state.dogBreed.isNotEmpty() && - isAgeValid(state.ageKnown, state.dogAge) - } - - val hideKeyboardAndClearFocus = { - keyboardController?.hide() - focusManager.clearFocus() - } - - val proceedToNext = { - if (isFormValid) { - hideKeyboardAndClearFocus() - navigateNext() - } - } - - val requestFocusSafely = { focusRequester: FocusRequester -> - try { - focusRequester.requestFocus() - } catch (e: Exception) { - - } - } - - Box( - modifier = modifier - .fillMaxSize() - ) { - Column(modifier = Modifier.fillMaxSize()) { - // 헤더 - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text( - text = stringResource(id = R.string.ic_onboarding_signup), - color = PawKeyTheme.colors.black, - style = PawKeyTheme.typography.body16Sb, - modifier = Modifier.padding(top = 16.dp), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - LinearProgressIndicator( - progress = { animatedProgress }, - modifier = Modifier - .fillMaxWidth() - .height(2.dp), - color = PawKeyTheme.colors.green500, - trackColor = PawKeyTheme.colors.gray100, - strokeCap = StrokeCap.Square, - gapSize = 0.dp, - drawStopIndicator = {} - ) - } - - LazyColumn( - verticalArrangement = Arrangement.spacedBy(32.dp), - contentPadding = PaddingValues(16.dp), - modifier = Modifier.fillMaxSize() - ) { - item { - Text( - text = stringResource(id = R.string.ic_onboarding_signup_subtitle_step3), - color = PawKeyTheme.colors.black, - style = PawKeyTheme.typography.head22Sb, - modifier = Modifier.padding(top = 20.dp) - ) - } - - item { - DogProfileImage( - dogImage = state.dogImage, - onClickImage = onClickImage - ) - } - - item { - DogNameField( - value = state.dogName, - onValueChange = viewModel::onDogNameChanged, - focusRequester = dogNameFocusRequester, - onNext = { requestFocusSafely(dogBreedFocusRequester) } - ) - } - - item { - Column { - DogGenderSection( - selectedGender = state.dogGender, - onGenderSelected = { gender -> - viewModel.selectDogGender(gender) - requestFocusSafely(dogBreedFocusRequester) - } - ) - Spacer(modifier = Modifier.height(10.dp)) - NeuteringCheckbox( - isNeutered = state.isNeutered, - onToggle = viewModel::toggleNeutering - ) - } - } - - item { - DogBreedField( - value = state.dogBreed, - onValueChange = viewModel::onDogBreedChanged, - focusRequester = dogBreedFocusRequester, - onNext = { focusManager.clearFocus() } - ) - } - - item { - DogAgeSection( - ageKnown = state.ageKnown, - dogAge = state.dogAge, - onAgeKnownSelected = { ageKnown -> - viewModel.selectAgeKnown(ageKnown) - if (ageKnown == SignUpContract.AgeKnown.KNOWN) { - requestFocusSafely(dogAgeFocusRequester) - } - }, - onDogAgeChanged = viewModel::onDogAgeChanged, - focusRequester = dogAgeFocusRequester, - onDone = proceedToNext - ) - } - - item { Spacer(modifier = Modifier.height(80.dp)) } - } - } - - PawkeyButton( - text = "다음으로", - enabled = isFormValid, - onClick = proceedToNext, - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 46.dp) - ) - } -} - -@Composable -private fun DogProfileImage( - dogImage: Uri?, - onClickImage: () -> Unit -) { - Column { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxWidth() - .height(96.dp) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(96.dp) - .clip(CircleShape) - .border( - width = 2.dp, - color = if (dogImage != null) PawKeyTheme.colors.green500 else PawKeyTheme.colors.white2, - shape = CircleShape - ) - .background(PawKeyTheme.colors.white2) - .noRippleClickable { onClickImage() } - ) { - if (dogImage != null) { - AsyncImage( - model = dogImage, - contentDescription = "강아지 프로필 이미지", - contentScale = ContentScale.Crop, - modifier = Modifier - .size(92.dp) - .clip(CircleShape) - ) - } else { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_onboarding_img_plus), - contentDescription = "앨범", - tint = PawKeyTheme.colors.gray100 - ) - } - } - } - } -} - -@Composable -private fun DogNameField( - value: String, - onValueChange: (String) -> Unit, - focusRequester: FocusRequester, - onNext: () -> Unit -) { - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_dog_name), - content = { - SignUpTextField( - value = value, - onValueChange = onValueChange, - placeholder = "강아지 이름을 입력해주세요", - modifier = Modifier.focusRequester(focusRequester), - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Text - ), - keyboardActions = KeyboardActions( - onNext = { onNext() } - ) - ) - } - ) -} - -@Composable -private fun DogGenderSection( - selectedGender: SignUpContract.DogGender, - onGenderSelected: (SignUpContract.DogGender) -> Unit, -) { - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_gender), - content = { - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.fillMaxWidth() - ) { - SignUpUserSelectButton( - user = "남성", - isSelect = selectedGender == SignUpContract.DogGender.MALE, - onClick = { onGenderSelected(SignUpContract.DogGender.MALE) }, - modifier = Modifier.weight(1f) - ) - SignUpUserSelectButton( - user = "여성", - isSelect = selectedGender == SignUpContract.DogGender.FEMALE, - onClick = { onGenderSelected(SignUpContract.DogGender.FEMALE) }, - modifier = Modifier.weight(1f) - ) - } - } - ) -} - -@Composable -private fun NeuteringCheckbox( - isNeutered: Boolean, - onToggle: () -> Unit, -) { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { onToggle() } - ) { - Icon( - imageVector = if (isNeutered) - ImageVector.vectorResource(R.drawable.ic_roundcheck_valid) - else - ImageVector.vectorResource(R.drawable.ic_roundcheck_invalid), - contentDescription = "", - tint = Color.Unspecified - ) - Text( - text = "중성화했어요", - color = if (isNeutered) - PawKeyTheme.colors.black - else PawKeyTheme.colors.gray300, - style = PawKeyTheme.typography.body14Sb - ) - } -} - -@Composable -private fun DogBreedField( - value: String, - onValueChange: (String) -> Unit, - focusRequester: FocusRequester, - onNext: () -> Unit -) { - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_dog_breed), - content = { - SignUpTextField( - value = value, - onValueChange = onValueChange, - placeholder = "견종을 입력해주세요", - modifier = Modifier.focusRequester(focusRequester), - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Text - ), - keyboardActions = KeyboardActions( - onNext = { onNext() } - ) - ) - } - ) -} - -@Composable -private fun DogAgeSection( - ageKnown: SignUpContract.AgeKnown, - dogAge: String, - onAgeKnownSelected: (SignUpContract.AgeKnown) -> Unit, - onDogAgeChanged: (String) -> Unit, - focusRequester: FocusRequester, - onDone: () -> Unit -) { - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_age), - content = { - Column( - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.fillMaxWidth() - ) { - SignUpUserSelectButton( - user = "나이를 알아요", - isSelect = ageKnown == SignUpContract.AgeKnown.KNOWN, - onClick = { - onAgeKnownSelected(SignUpContract.AgeKnown.KNOWN) - onDogAgeChanged("") - }, - modifier = Modifier.weight(1f) - ) - - SignUpUserSelectButton( - user = "나이를 몰라요", - isSelect = ageKnown == SignUpContract.AgeKnown.UNKNOWN, - onClick = { - onAgeKnownSelected(SignUpContract.AgeKnown.UNKNOWN) - onDogAgeChanged("") - }, - modifier = Modifier.weight(1f) - ) - } - - if (ageKnown == SignUpContract.AgeKnown.KNOWN) { - SignUpTextField( - value = dogAge, - onValueChange = onDogAgeChanged, - placeholder = "나이를 입력해주세요", - modifier = Modifier.focusRequester(focusRequester), - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Number - ), - keyboardActions = KeyboardActions( - onDone = { onDone() } - ) - ) - } - } - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpLevelScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpLevelScreen.kt deleted file mode 100644 index e314bad4..00000000 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpLevelScreen.kt +++ /dev/null @@ -1,203 +0,0 @@ -package com.paw.key.presentation.ui.signup - -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.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.paw.key.R -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.presentation.ui.signup.component.SignUpHeader -import com.paw.key.presentation.ui.signup.component.SignUpUserSelectButton -import com.paw.key.presentation.ui.signup.state.SignUpContract -import com.paw.key.presentation.ui.signup.viewmodel.SignUpViewModel - -@Preview(showBackground = true) -@Composable -private fun PreviewSignUpLevelScreen() { - PawKeyTheme { - SignUpLevelScreen( - onSignUpClick = {}, - selectedEnergyLevel = "", - selectedSocialLevel = "", - energyOptions = listOf("매우 차분해요", "조금 느긋해요"), - socialOptions = listOf("잘 어울려요", "천천히 친해져요"), - energyTitle = "에너지 레벨", - socialTitle = "사회성 레벨", - viewModel = hiltViewModel() - ) - } -} - -@Composable -fun SignUpLevelRoute( - navigateNext: () -> Unit, - modifier: Modifier = Modifier, - viewModel: SignUpViewModel? = null -) { - val actualViewModel = viewModel ?: hiltViewModel() - val state by actualViewModel.state.collectAsState() - val context = LocalContext.current - - LaunchedEffect(actualViewModel.sideEffect) { - actualViewModel.sideEffect.collect { sideEffect -> - when (sideEffect) { - is SignUpContract.SignUpSideEffect.NavigateNext -> { - navigateNext() - } - is SignUpContract.SignUpSideEffect.ShowSnackBar -> { - println("SnackBar: ${sideEffect.message}") - } - is SignUpContract.SignUpSideEffect.NavigateUp -> { - - } - } - } - } - - val energyCategory = state.petTraitCategoryList.find { it.petTraitCategoryName == "에너지레벨" } - val socialCategory = state.petTraitCategoryList.find { it.petTraitCategoryName == "사회성레벨" } - - val energyOptions = energyCategory?.petTraitCategoryOptions?.map { it.petTraitCategoryOptionText } ?: emptyList() - val socialOptions = socialCategory?.petTraitCategoryOptions?.map { it.petTraitCategoryOptionText } ?: emptyList() - - val isButtonEnabled = state.selectedEnergyLevel.isNotEmpty() && - state.selectedSocialLevel.isNotEmpty() - - SignUpLevelScreen( - enabled = isButtonEnabled, - selectedEnergyLevel = state.selectedEnergyLevel, - selectedSocialLevel = state.selectedSocialLevel, - onSignUpClick = { - println("SignUp button clicked") - actualViewModel.debugSignUpState() - actualViewModel.signUp(context) - }, - energyOptions = energyOptions, - socialOptions = socialOptions, - energyTitle = "에너지 레벨", - socialTitle = "사회성 레벨", - modifier = modifier, - viewModel = actualViewModel - ) -} - -@Composable -fun SignUpLevelScreen( - onSignUpClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = false, - selectedEnergyLevel: String = "", - selectedSocialLevel: String = "", - energyOptions: List, - socialOptions: List, - energyTitle: String = "에너지 레벨", - socialTitle: String = "사회성 레벨", - viewModel: SignUpViewModel -) { - Column( - modifier = modifier.fillMaxSize() - ) { - SignUpHeader( - title = stringResource(id = R.string.ic_onboarding_signup), - subtitle = stringResource(id = R.string.ic_onboarding_signup_subtitle_step4) - ) - - Spacer(modifier = Modifier.height(42.dp)) - - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) - ) { - LevelSection( - title = energyTitle, - options = energyOptions.chunked(2), - selectedOption = selectedEnergyLevel, - onOptionClick = { option -> - println("Energy level selected: $option") - viewModel.selectEnergyLevel(option) - } - ) - - Spacer(modifier = Modifier.height(36.dp)) - - LevelSection( - title = socialTitle, - options = socialOptions.chunked(2), - selectedOption = selectedSocialLevel, - onOptionClick = { option -> - println("Social level selected: $option") - viewModel.selectSocialLevel(option) - } - ) - - Spacer(modifier = Modifier.weight(1f)) - - PawkeyButton( - text = stringResource(id = R.string.ic_onboarding_signup_button), - enabled = enabled, - onClick = { - println("Button clicked, enabled: $enabled") - onSignUpClick() - } - ) - - Spacer(modifier = Modifier.height(46.dp)) - } - } -} - -@Composable -private fun LevelSection( - title: String, - options: List>, - selectedOption: String, - onOptionClick: (String) -> Unit, -) { - Column { - Text( - text = title, - style = PawKeyTheme.typography.body14Sb, - color = PawKeyTheme.colors.black - ) - - Spacer(modifier = Modifier.height(12.dp)) - - options.forEach { rowOptions -> - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 10.dp) - ) { - rowOptions.forEach { option -> - SignUpUserSelectButton( - user = option, - isSelect = selectedOption == option, - onClick = { - println("Option clicked: $option") - onOptionClick(option) - }, - modifier = Modifier.weight(1f) - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpLocationInfoScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpLocationInfoScreen.kt new file mode 100644 index 00000000..7a642951 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpLocationInfoScreen.kt @@ -0,0 +1,88 @@ +package com.paw.key.presentation.ui.signup + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.paw.key.R +import com.paw.key.core.designsystem.component.PawKeyBottomSheet +import com.paw.key.core.extension.noRippleClickable +import com.paw.key.presentation.ui.signup.component.FormField +import com.paw.key.presentation.ui.signup.component.RegionSearchContent +import com.paw.key.presentation.ui.signup.component.SignUpTextField +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SignUpLocationInfoScreen( + gu : String, + dong : String, + onSelectedLocation : (gu : String, dong : String) -> Unit, + modifier: Modifier = Modifier +) { + val scope = rememberCoroutineScope() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var isSheetOpen by remember { mutableStateOf(false) } + + Column ( + modifier = modifier + .padding( + top = 40.dp, + start = 16.dp, + end = 16.dp + ) + ) { + FormField( + label = "선택 지역", + content = { + SignUpTextField( + value = if (gu.isNotEmpty() && dong.isNotEmpty()) "$gu $dong" else "", + onValueChange = {}, + enabled = false, + placeholder = "주로 산책하는 지역을 검색해보세요", + suffix = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_signup_search), + contentDescription = "breed search", + tint = Color.Unspecified + ) + }, + modifier = Modifier + .noRippleClickable { + scope.launch { + isSheetOpen = true + } + } + ) + } + ) + } + + if (isSheetOpen) { + PawKeyBottomSheet( + sheetState = sheetState, + onDismissRequest = { isSheetOpen = false }, + ) { + RegionSearchContent( + selectedGu = gu, + selectedDong = dong, + onRegionSelected = { gu, dong -> + isSheetOpen = false + onSelectedLocation(gu, dong) + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpMapInfoScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpMapInfoScreen.kt new file mode 100644 index 00000000..77fa811f --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpMapInfoScreen.kt @@ -0,0 +1,161 @@ +package com.paw.key.presentation.ui.signup + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.naver.maps.geometry.LatLng +import com.naver.maps.geometry.LatLngBounds +import com.naver.maps.map.CameraUpdate +import com.naver.maps.map.compose.ExperimentalNaverMapApi +import com.naver.maps.map.compose.NaverMap +import com.naver.maps.map.compose.PolygonOverlay +import com.naver.maps.map.compose.rememberCameraPositionState +import com.paw.key.core.designsystem.component.PawkeyButton +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.presentation.ui.region.state.DrawType +import kotlinx.collections.immutable.ImmutableList + + +@OptIn(ExperimentalNaverMapApi::class) +@Composable +fun SignUpMapInfoScreen( + type: DrawType, + regionCoordinates: ImmutableList>, + entireCoordinates: ImmutableList, + regionName: String, + onClickButton: () -> Unit, + modifier: Modifier = Modifier, +) { + val cameraPositionState = rememberCameraPositionState() + val bottomPanelHeightPx = remember { + mutableIntStateOf(0) + } + + val density = LocalDensity.current + + LaunchedEffect(entireCoordinates.size) { + if (entireCoordinates.size >= 2 && bottomPanelHeightPx.intValue > 0) { + val bounds = LatLngBounds.from(entireCoordinates) + + val bottomPadding = + bottomPanelHeightPx.intValue + with(density) { 100.dp.roundToPx() } + + cameraPositionState.move( + CameraUpdate.fitBounds(bounds, 100, 100, 100, bottomPadding) + ) + } + } + + Box( + modifier = modifier + .fillMaxSize() + ) { + NaverMap( + modifier = Modifier + .fillMaxSize() + .align(Alignment.Center), + cameraPositionState = cameraPositionState + ) { + when (type) { + DrawType.SINGLE -> { + val singlePolygonCoords = regionCoordinates.first() + if (singlePolygonCoords.isNotEmpty()) { + PolygonOverlay( + coords = singlePolygonCoords, + color = PawKeyTheme.colors.opacityPrimary.copy(alpha = 0.3f), + outlineWidth = 1.dp, + outlineColor = PawKeyTheme.colors.green500 + ) + } + } + + DrawType.MULTIPLE -> { + regionCoordinates.forEach { + PolygonOverlay( + coords = it, + color = PawKeyTheme.colors.opacityPrimary.copy(alpha = 0.3f), + outlineWidth = 1.dp, + outlineColor = PawKeyTheme.colors.green500 + ) + } + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) + .background( + color = PawKeyTheme.colors.white1, + shape = RoundedCornerShape( + topStart = 16.dp, topEnd = 16.dp + ) + ) + .padding(horizontal = 16.dp, vertical = 24.dp) + .onSizeChanged { size -> + bottomPanelHeightPx.intValue = size.height + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "선택한 위치", + style = PawKeyTheme.typography.header3, + color = PawKeyTheme.colors.contents + ) + + Text( + text = regionName, + style = PawKeyTheme.typography.header3, + color = PawKeyTheme.colors.primary + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = "선택한 산책 지역은 ${regionName}이에요.\n이 위치로 산책 지역을 설정하시겠어요?", + style = PawKeyTheme.typography.bodyDefault, + color = PawKeyTheme.colors.gray500, + modifier = Modifier + .padding(bottom = 12.dp) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + PawkeyButton( + text = "선택", + onClick = onClickButton, + modifier = Modifier + .fillMaxWidth(), + enabled = true, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpPetInfoScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpPetInfoScreen.kt new file mode 100644 index 00000000..71f44d03 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpPetInfoScreen.kt @@ -0,0 +1,247 @@ +package com.paw.key.presentation.ui.signup + +import android.Manifest +import android.net.Uri +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.paw.key.R +import com.paw.key.core.designsystem.component.PawKeyBottomSheet +import com.paw.key.core.extension.noRippleClickable +import com.paw.key.core.util.DateVisualTransformation +import com.paw.key.presentation.ui.signup.component.FormField +import com.paw.key.presentation.ui.signup.component.GenderSelector +import com.paw.key.presentation.ui.signup.component.PetBreedSearchContent +import com.paw.key.presentation.ui.signup.component.SignUpNeuteringCheckRadio +import com.paw.key.presentation.ui.signup.component.SignUpPetImageHolder +import com.paw.key.presentation.ui.signup.component.SignUpTextField +import com.paw.key.presentation.ui.signup.state.Gender +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SignUpPetInfoScreen( + petName : String, + petBirthDate : String, + petGender : Gender, + petNeutered : Boolean, + petBreed : String, + selectedImageUri: Uri?, + deniedPermission: () -> Unit, + onPetNameChanged : (String) -> Unit, + onPetBirthDateChanged : (String) -> Unit, + onPetGenderChanged : (Gender) -> Unit, + onPetNeuteredChanged : (Boolean) -> Unit, + onPetBreedChanged : (String) -> Unit, + onSelectedImage: (Uri?) -> Unit, + modifier: Modifier = Modifier +) { + var isSheetOpen by remember { mutableStateOf(false) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scope = rememberCoroutineScope() + + val petBirthDateFocusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + val photoPickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri -> + onSelectedImage(uri) + } + ) + + // 구버전 권한 요청용 + val legacyGalleryLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + onResult = { uri -> + onSelectedImage(uri) + } + ) + + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { isGranted -> + if (isGranted) { + legacyGalleryLauncher.launch("image/*") + } else { + deniedPermission + } + } + ) + + Column( + modifier = modifier + .padding( + top = 40.dp, + start = 16.dp, + end = 16.dp + ) + ) { + SignUpPetImageHolder( + uri = selectedImageUri, + modifier = Modifier + .noRippleClickable { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + photoPickerLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + } else { + permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) + } + } + ) + + Spacer(modifier = Modifier.height(20.dp)) + + FormField( + label = "이름", + content = { + SignUpTextField( + value = petName, + onValueChange = { + if (it.length <= 8) { + onPetNameChanged(it) + } + }, + placeholder = "최대 8글자 이내로 입력해주세요", + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onDone = { + petBirthDateFocusRequester.requestFocus() + } + ), + ) + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + FormField( + label = "생년월일", + content = { + SignUpTextField( + modifier = Modifier + .focusRequester(petBirthDateFocusRequester), + value = petBirthDate, + onValueChange = { + if (it.length <= 8) { + onPetBirthDateChanged(it) + } + }, + placeholder = "YYYYMMDD", + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + } + ), + visualTransformation = DateVisualTransformation() + ) + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + FormField( + label = "성별", + content = { + GenderSelector( + selectedGender = petGender, + onGenderSelected = onPetGenderChanged, + type = "반려 동물" + ) + } + ) + + SignUpNeuteringCheckRadio( + isNeutered = petNeutered, + onToggle = { onPetNeuteredChanged(!petNeutered) }, + modifier = Modifier + .padding(top = 8.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + FormField( + label = "견종", + content = { + SignUpTextField( + value = petBreed, + onValueChange = onPetBreedChanged, + enabled = false, + placeholder = "견종을 검색해보세요", + suffix = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_signup_search), + contentDescription = "breed search", + tint = Color.Unspecified + ) + }, + modifier = Modifier + .noRippleClickable { + scope.launch { + isSheetOpen = true + } + } + ) + } + ) + + Spacer(modifier = Modifier.height(24.dp)) + + if (isSheetOpen) { + PawKeyBottomSheet( + onDismissRequest = { isSheetOpen = false }, + sheetState = sheetState, + //sheetGesturesEnabled = false, + ) { sheetState -> + PetBreedSearchContent( + sheetState = sheetState, + selectedBreed = petBreed, + onBreedSelected = { + onPetBreedChanged(it) + scope.launch { + + sheetState.hide() + }.invokeOnCompletion { + if (!sheetState.isVisible) { + isSheetOpen = false + } + } + }, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt index e63e76b3..70ffad7a 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpScreen.kt @@ -1,219 +1,257 @@ package com.paw.key.presentation.ui.signup -import androidx.compose.foundation.layout.Arrangement +import android.net.Uri +import androidx.activity.compose.BackHandler 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.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.paw.key.R -import com.paw.key.core.designsystem.component.PawkeyButton -import com.paw.key.core.designsystem.theme.PawKeyTheme -import com.paw.key.presentation.ui.signup.component.FormField +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import com.paw.key.core.designsystem.component.DogkyButton +import com.paw.key.core.designsystem.component.LoadingScreen +import com.paw.key.core.util.UiState import com.paw.key.presentation.ui.signup.component.SignUpHeader -import com.paw.key.presentation.ui.signup.component.SignUpTextField -import com.paw.key.presentation.ui.signup.component.SignUpUserSelectButton -import com.paw.key.presentation.ui.signup.state.SignUpContract +import com.paw.key.presentation.ui.signup.component.SignUpSubHeader +import com.paw.key.presentation.ui.signup.model.SignUpLocationInfo +import com.paw.key.presentation.ui.signup.model.SignUpMapInfo +import com.paw.key.presentation.ui.signup.model.SignUpPetInfo +import com.paw.key.presentation.ui.signup.model.SignUpUserInfo +import com.paw.key.presentation.ui.signup.state.Gender +import com.paw.key.presentation.ui.signup.state.SignUpSideEffect +import com.paw.key.presentation.ui.signup.state.SignUpStateType import com.paw.key.presentation.ui.signup.viewmodel.SignUpViewModel -@Preview(showBackground = true) @Composable -private fun PreviewSignUpScreen() { - PawKeyTheme { - SignUpScreen( - step = 0.25F, - navigateSignUpActivity = {}, - viewModel = hiltViewModel() - ) +fun SignUpRoute( + navigateUp: () -> Unit, + navigateToHome: () -> Unit, + viewModel: SignUpViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val lifecycleOwner = LocalLifecycleOwner.current + + BackHandler(enabled = true) { + viewModel.onBackPressed() } -} -@Composable -fun SignUpRoute( - navigateSignUpActivity: () -> Unit, - modifier: Modifier = Modifier, - viewModel: SignUpViewModel? = null - ) { - val actualViewModel = viewModel ?: hiltViewModel() + LaunchedEffect(Unit) { + viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) + .collect { it -> + when (it) { + is SignUpSideEffect.NavigateUp -> { + navigateUp() + } + is SignUpSideEffect.ShowSnackBar -> { + + } + is SignUpSideEffect.NavigateNext -> { + viewModel.updateStep() + } + is SignUpSideEffect.NavigateHome -> { + navigateToHome() + } + } + } + } SignUpScreen( - step = 0.25F, - navigateSignUpActivity = navigateSignUpActivity, - modifier = modifier, - viewModel = actualViewModel + currentStep = state.currentStep, + type = state.signUpState, + isNextEnabled = state.isNextEnabled, + + onNextClick = viewModel::onNextClick, + onBackClick = viewModel::onBackPressed, + + userInfo = state.userInfo, + onNickNameChanged = { viewModel.updateNickname(it) }, + onBirthDateChanged = { viewModel.updateBirthDate(it) }, + onGenderChanged = viewModel::updateGender, + + petInfo = state.petInfo, + onPetNameChanged = { viewModel.updatePetName(it) }, + onPetBirthDateChanged = { viewModel.updatePetBirthDate(it) }, + onPetGenderChanged = viewModel::updatePetGender, + onPetNeuteredChanged = viewModel::updatePetNeutered, + onPetBreedChanged = { viewModel.updatePetBreed(it) }, + deniedPermission = viewModel::deniedPermission, + onSelectedImage = viewModel::updatePetImage, + + locationInfo = state.locationInfo, + onSelectedLocation = { gu, dong -> + viewModel.onRegionSelected(gu, dong) + }, + + mapInfo = state.mapInfo, ) } @Composable fun SignUpScreen( - step: Float, - navigateSignUpActivity: () -> Unit, - modifier: Modifier = Modifier, - viewModel: SignUpViewModel + userInfo : SignUpUserInfo, + onNickNameChanged: (String) -> Unit, + onBirthDateChanged: (String) -> Unit, + onGenderChanged: (Gender) -> Unit, + + petInfo : SignUpPetInfo, + deniedPermission: () -> Unit, + onPetNameChanged : (String) -> Unit, + onPetBirthDateChanged : (String) -> Unit, + onPetGenderChanged : (Gender) -> Unit, + onPetNeuteredChanged : (Boolean) -> Unit, + onPetBreedChanged : (String) -> Unit, + onSelectedImage: (Uri?) -> Unit, + + locationInfo : SignUpLocationInfo, + onSelectedLocation : (gu : String, dong : String) -> Unit, + + mapInfo: SignUpMapInfo, + + isNextEnabled: Boolean, + onNextClick: () -> Unit, + onBackClick: () -> Unit, + currentStep : Float, + type: SignUpStateType, ) { - val state by viewModel.state.collectAsState() - val keyboardController = LocalSoftwareKeyboardController.current - val focusManager = LocalFocusManager.current + val title = when(type) { + SignUpStateType.USER_INFO -> { + "내 정보 입력" + } + SignUpStateType.PET_INFO -> { + "반려견 정보 입력" + } + SignUpStateType.LOCATION_INFO -> { + "산책 지역 입력" + } + SignUpStateType.REGION_MANAGEMENT -> { + "산책 지역 입력" + } + } - // FocusRequester들 생성 - val nameFocusRequester = remember { FocusRequester() } - val ageFocusRequester = remember { FocusRequester() } + val buttonText = when(type) { + SignUpStateType.LOCATION_INFO -> { + "완료" + } + else -> { + "다음" + } + } - LazyColumn( - modifier = modifier + Column ( + modifier = Modifier .fillMaxSize() - .imePadding() ) { - item { - SignUpHeader( - title = stringResource(R.string.ic_onboarding_signup), - subtitle = stringResource(id = R.string.ic_onboarding_signup_subtitle_step1), - progress = step, - ) - } + SignUpHeader( + title = title, + onBackClick = onBackClick, + progress = currentStep + ) - item { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - Spacer(modifier = Modifier.height(42.dp)) - - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_name), - content = { - SignUpTextField( - value = state.name, - onValueChange = viewModel::onNameChanged, - placeholder = "이름을 입력해주세요", - modifier = Modifier.focusRequester(nameFocusRequester), - keyboardOptions = KeyboardOptions( - imeAction = androidx.compose.ui.text.input.ImeAction.Next, - keyboardType = KeyboardType.Text - ), - keyboardActions = KeyboardActions( - onNext = { - focusManager.clearFocus() - } - ) - ) - } - ) - - Spacer(modifier = Modifier.height(33.dp)) - - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_gender), - content = { - GenderSelector( - selectedGender = state.selectedGender, - onGenderSelected = { gender -> - viewModel.selectGender(gender) - // 성별 선택 후 나이 입력으로 포커싱 - ageFocusRequester.requestFocus() - } - ) + when (type) { + SignUpStateType.REGION_MANAGEMENT -> { + when (val state = mapInfo.uiState) { + is UiState.Loading -> { + LoadingScreen() } - ) - - Spacer(modifier = Modifier.height(33.dp)) - - FormField( - label = stringResource(id = R.string.ic_onboarding_signup_age), - content = { - SignUpTextField( - value = state.age, - onValueChange = viewModel::onAgeChanged, - placeholder = "나이를 입력해주세요", - modifier = Modifier.focusRequester(ageFocusRequester), - keyboardOptions = KeyboardOptions( - imeAction = androidx.compose.ui.text.input.ImeAction.Done, - keyboardType = KeyboardType.Number - ), - keyboardActions = KeyboardActions( - onDone = { - keyboardController?.hide() - focusManager.clearFocus() - - // 폼이 유효하면 다음 단계로 - val isFormValid = state.name.isNotBlank() && - state.age.isNotBlank() && - state.selectedGender != SignUpContract.Gender.UNKNOWN - - if (isFormValid) { - navigateSignUpActivity() - } - } - ) + + is UiState.Success -> { + SignUpMapInfoScreen( + type = mapInfo.drawType, + regionCoordinates = state.data, + entireCoordinates = mapInfo.entireCoordinates, + regionName = mapInfo.regionName, + onClickButton = onNextClick, + modifier = Modifier ) } - ) - Spacer(modifier = Modifier.height(60.dp)) + else -> {} + } + } + + else -> { + SignUpSubHeader() + + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + ) { + when (type) { + SignUpStateType.USER_INFO -> { + SignUpUserInfoScreen( + nickName = userInfo.nickName, + birthDate = userInfo.birthDate, + gender = userInfo.gender, + onNickNameChanged = onNickNameChanged, + onBirthDateChanged = onBirthDateChanged, + onGenderChanged = onGenderChanged, + modifier = Modifier + .padding(top = 40.dp) + ) + } - val isFormValid = state.name.isNotBlank() && - state.age.isNotBlank() && - state.selectedGender != SignUpContract.Gender.UNKNOWN + SignUpStateType.PET_INFO -> { + SignUpPetInfoScreen( + petName = petInfo.petName, + petBirthDate = petInfo.petBirthDate, + petGender = petInfo.petGender, + petNeutered = petInfo.petNeutered, + petBreed = petInfo.petBreed, + selectedImageUri = petInfo.petImage, + onPetNameChanged = onPetNameChanged, + onPetBirthDateChanged = onPetBirthDateChanged, + onPetGenderChanged = onPetGenderChanged, + onPetNeuteredChanged = onPetNeuteredChanged, + onPetBreedChanged = onPetBreedChanged, + deniedPermission = deniedPermission, + onSelectedImage = { + onSelectedImage(it) + }, + modifier = Modifier + ) + } - PawkeyButton( - text = stringResource(id = R.string.ic_onboarding_signup_button), - enabled = isFormValid, - onClick = { - if (isFormValid) { - keyboardController?.hide() - navigateSignUpActivity() + SignUpStateType.LOCATION_INFO -> { + SignUpLocationInfoScreen( + gu = locationInfo.selectedGu, + dong = locationInfo.selectedDong, + onSelectedLocation = { gu, dong -> + onSelectedLocation(gu, dong) + }, + modifier = Modifier + ) } + + else -> {} } - ) - Spacer(modifier = Modifier.height(46.dp)) + Spacer(modifier = Modifier.weight(1f)) + + DogkyButton( + text = buttonText, + onClick = onNextClick, + enabled = isNextEnabled, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + end = 16.dp, + bottom = 32.dp + ) + ) + } } } } -} - -@Composable -private fun GenderSelector( - selectedGender: SignUpContract.Gender, - onGenderSelected: (SignUpContract.Gender) -> Unit, -) { - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.fillMaxWidth() - ) { - SignUpUserSelectButton( - user = "남성", - isSelect = selectedGender == SignUpContract.Gender.MALE, - onClick = { onGenderSelected(SignUpContract.Gender.MALE) }, - modifier = Modifier.weight(1f) - ) - - SignUpUserSelectButton( - user = "여성", - isSelect = selectedGender == SignUpContract.Gender.FEMALE, - onClick = { onGenderSelected(SignUpContract.Gender.FEMALE) }, - modifier = Modifier.weight(1f) - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpUserInfoScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpUserInfoScreen.kt new file mode 100644 index 00000000..ca726a70 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/SignUpUserInfoScreen.kt @@ -0,0 +1,105 @@ +package com.paw.key.presentation.ui.signup + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.paw.key.core.util.DateVisualTransformation +import com.paw.key.presentation.ui.signup.component.FormField +import com.paw.key.presentation.ui.signup.component.GenderSelector +import com.paw.key.presentation.ui.signup.component.SignUpTextField +import com.paw.key.presentation.ui.signup.state.Gender + +@Composable +fun SignUpUserInfoScreen( + nickName: String, + birthDate: String, + gender: Gender, + onNickNameChanged: (String) -> Unit, + onBirthDateChanged: (String) -> Unit, + onGenderChanged: (Gender) -> Unit, + modifier: Modifier = Modifier, +) { + val birthDateFocusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + Column ( + modifier = modifier + .padding(16.dp) + ) { + FormField( + label = "닉네임", + content = { + SignUpTextField( + value = nickName, + onValueChange = { + if (it.length <= 8) { + onNickNameChanged(it) + } + }, + placeholder = "최대 8글자 이내로 입력해주세요", + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { + birthDateFocusRequester.requestFocus() + } + ) + ) + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + FormField( + label = "생년월일", + content = { + SignUpTextField( + modifier = Modifier + .focusRequester(birthDateFocusRequester), + value = birthDate, + onValueChange = { + if (it.length <= 8) { + onBirthDateChanged(it) + } + }, + placeholder = "YYYYMMDD", + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + focusManager.clearFocus() + } + ), + visualTransformation = DateVisualTransformation() + ) + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + FormField( + label = "성별", + content = { + GenderSelector( + selectedGender = gender, + onGenderSelected = onGenderChanged + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/GenderSelector.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/GenderSelector.kt new file mode 100644 index 00000000..a17d497f --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/GenderSelector.kt @@ -0,0 +1,66 @@ +package com.paw.key.presentation.ui.signup.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.presentation.ui.signup.state.Gender + +@Composable +fun GenderSelector( + selectedGender: Gender, + onGenderSelected: (Gender) -> Unit, + modifier: Modifier = Modifier, + type : String? = "유저", +) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier.fillMaxWidth() + ) { + if (type == "유저") { + SignUpUserSelectButton( + user = "남성", + isSelect = selectedGender == Gender.MALE, + onClick = { onGenderSelected(Gender.MALE) }, + modifier = Modifier.weight(1f) + ) + + SignUpUserSelectButton( + user = "여성", + isSelect = selectedGender == Gender.FEMALE, + onClick = { onGenderSelected(Gender.FEMALE) }, + modifier = Modifier.weight(1f) + ) + } else { + SignUpUserSelectButton( + user = "남아", + isSelect = selectedGender == Gender.MALE, + onClick = { onGenderSelected(Gender.MALE) }, + modifier = Modifier.weight(1f) + ) + + SignUpUserSelectButton( + user = "여아", + isSelect = selectedGender == Gender.FEMALE, + onClick = { onGenderSelected(Gender.FEMALE) }, + modifier = Modifier.weight(1f) + ) + } + } +} + +@Preview +@Composable +private fun GenderSelectorPreview() { + PawKeyTheme { + GenderSelector( + selectedGender = Gender.MALE, + onGenderSelected = {}, + type = "성별" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/NeuteringCheckbox.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/NeuteringCheckbox.kt new file mode 100644 index 00000000..c4823b22 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/NeuteringCheckbox.kt @@ -0,0 +1,56 @@ +package com.paw.key.presentation.ui.signup.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Icon +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.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.R +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable + +@Composable +fun SignUpNeuteringCheckRadio( + isNeutered: Boolean, + onToggle: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier + .fillMaxWidth() + .noRippleClickable(onToggle) + ) { + Icon( + imageVector = if (isNeutered) + ImageVector.vectorResource(R.drawable.ic_roundcheck_valid) + else + ImageVector.vectorResource(R.drawable.ic_roundcheck_invalid), + contentDescription = "neutering check", + tint = Color.Unspecified + ) + Text( + text = "중성화 했어요", + color = PawKeyTheme.colors.default, + style = PawKeyTheme.typography.bodySmall + ) + } +} + +@Preview +@Composable +private fun NeuteringCheckRadioPreview() { + PawKeyTheme { + SignUpNeuteringCheckRadio( + isNeutered = true, + onToggle = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/PetBreedSearchContent.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/PetBreedSearchContent.kt new file mode 100644 index 00000000..123faac9 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/PetBreedSearchContent.kt @@ -0,0 +1,196 @@ +package com.paw.key.presentation.ui.signup.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.R +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable +import kotlinx.coroutines.launch + +private val dummyPetBreeds = listOf( + "닥스훈트", "달마시안", "말라뮤트", "말티즈", "믹스견", + "미니핀", "보스턴 테리어", "불독", "비글", "비숑 프리제", + "사모예드", "시바견", "시츄", "요크셔 테리어", "진돗개", + "치와와", "포메라니안", "푸들" +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PetBreedSearchContent( + sheetState: SheetState, + selectedBreed : String, + onBreedSelected : (String) -> Unit, + modifier: Modifier = Modifier +) { + // 바텀 시트용 + var bottomPetBreed by remember { + mutableStateOf("") + } + + val scope = rememberCoroutineScope() + + Column ( + modifier = modifier + .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) + .fillMaxWidth() + .fillMaxHeight(0.7f) + .background( + color = PawKeyTheme.colors.background2, + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp) + ) + .padding(horizontal = 16.dp) + ) { + Text( + text = "견종 검색", + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.contents, + modifier = Modifier + .fillMaxWidth() + .padding( + top = 24.dp, + bottom = 24.dp + ), + textAlign = TextAlign.Center + ) + + SignUpTextField( + value = bottomPetBreed, + onValueChange = { + bottomPetBreed = it + }, + placeholder = "견종을 검색해보세요", + suffix = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_signup_search), + contentDescription = "breed search", + tint = PawKeyTheme.colors.contents + ) + }, + modifier = Modifier.onFocusChanged { focusState -> + if (focusState.isFocused && sheetState.currentValue != SheetValue.Expanded) { + scope.launch { + sheetState.expand() + } + } + } + ) + + PetBreedSearchList( + breedList = dummyPetBreeds, + petBreed = bottomPetBreed, + selectedBreed = selectedBreed, + onBreedSelected = onBreedSelected, + modifier = Modifier + .weight(1f) + ) + } +} + +@Composable +fun PetBreedSearchList( + breedList : List, + petBreed : String, + selectedBreed : String, + onBreedSelected : (String) -> Unit, + modifier: Modifier = Modifier +) { + val filteredList = if (petBreed.isBlank()) { + breedList + } else { + breedList.filter { it.contains(petBreed, ignoreCase = true) } + } + + LazyColumn( + modifier = modifier + .padding(top = 8.dp) + ) { + itemsIndexed( + items = filteredList + ) { index, item -> + PetBreedSearchItem( + petBreed = item, + onBreedSelected = onBreedSelected, + isPetBreedSelected = petBreed == item || selectedBreed == item, + ) + } + } +} + +@Composable +fun PetBreedSearchItem( + petBreed : String, + onBreedSelected : (String) -> Unit, + isPetBreedSelected : Boolean, + modifier: Modifier = Modifier +) { + val textColor = if (isPetBreedSelected) { + PawKeyTheme.colors.background2 + } else { + PawKeyTheme.colors.contents + } + + val backgroundColor = if (isPetBreedSelected) { + PawKeyTheme.colors.primary + } else { + PawKeyTheme.colors.background2 + } + + Text( + text = petBreed, + style = PawKeyTheme.typography.bodyActive, + color = textColor, + modifier = modifier + .fillMaxWidth() + .background( + color = backgroundColor, + shape = RoundedCornerShape(8.dp) + ) + .padding( + horizontal = 16.dp, + vertical = 11.dp + ) + .noRippleClickable { + onBreedSelected(petBreed) + }, + textAlign = TextAlign.Start + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +private fun PetPetBreedSearchContentPreview() { + PawKeyTheme { + PetBreedSearchContent( + selectedBreed = "", + onBreedSelected = {}, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/RegionSearchContent.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/RegionSearchContent.kt new file mode 100644 index 00000000..3f5d44b8 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/RegionSearchContent.kt @@ -0,0 +1,278 @@ +package com.paw.key.presentation.ui.signup.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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +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.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.R +import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable + +private val dummySeoulRegions = listOf( + Pair( + "강남구", listOf( + "개포동", "논현동", "대치동", "도곡동", "삼성동", "세곡동", "수서동", + "신사동", "압구정동", "역삼동", "율현동", "일원동", "자곡동", "청담동" + ) + ), + Pair( + "구로구", listOf( + "가리봉동", "개봉동", "고척동", "구로동", "궁동", "신도림동", "오류동", + "온수동", "천왕동", "항동" + ) + ), + Pair( + "금천구", listOf( + "가산동", "독산동", "시흥동" + ) + ), + Pair( + "노원구", listOf( + "공릉동", "상계동", "월계동", "중계동", "하계동" + ) + ), + Pair( + "도봉구", listOf( + "도봉동", "방학동", "쌍문동", "창동" + ) + ), + Pair( + "동대문구", listOf( + "답십리동", "신설동", "용두동", "이문동", "장안동", "전농동", "제기동", + "청량리동", "회기동", "휘경동" + ) + ), + Pair( + "동작구", listOf( + "노량진동", "대방동", "동작동", "본동", "사당동", "상도1동", "상도동", + "신대방동", "흑석동" + ) + ) +) + +@Composable +fun RegionSearchContent( + selectedGu: String, + selectedDong: String, + onRegionSelected: (gu: String, dong: String) -> Unit, + modifier: Modifier = Modifier, +) { + var searchQuery by remember { mutableStateOf("") } + + var selectedGu by remember { + mutableStateOf(selectedGu) + } + + var selectedDong by remember { + mutableStateOf(selectedDong) + } + + val filteredRegions = remember(searchQuery) { + if (searchQuery.isBlank()) { + dummySeoulRegions + } else { + dummySeoulRegions.mapNotNull { (gu, dongList) -> + val matchingDongs = dongList.filter { dong -> + "$gu $dong".contains(searchQuery, ignoreCase = true) + } + + if (matchingDongs.isNotEmpty()) { + Pair(gu, matchingDongs) + } else { + null + } + } + } + } + + val dongListForSelectedGu = remember(selectedGu, filteredRegions) { + filteredRegions.find { it.first == selectedGu }?.second ?: emptyList() + } + + Column( + modifier = modifier + .clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)) + .fillMaxWidth() + .fillMaxHeight(0.7f) + .background( + color = PawKeyTheme.colors.background2, + shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp) + ) + .padding(horizontal = 16.dp) + ) { + Text( + text = "산책 지역", + style = PawKeyTheme.typography.subTitle, + color = PawKeyTheme.colors.contents, + modifier = Modifier + .fillMaxWidth() + .padding( + top = 24.dp, + bottom = 24.dp + ), + textAlign = TextAlign.Center + ) + + SignUpTextField( + value = searchQuery, + onValueChange = { + searchQuery = it + }, + placeholder = "지역을 검색해보세요", + suffix = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_signup_search), + contentDescription = "region search", + tint = PawKeyTheme.colors.contents + ) + } + ) + + RegionSearchList( + guList = filteredRegions.map { it.first }, + dongList = dongListForSelectedGu, + selectedGu = selectedGu, + selectedDong = selectedDong, + onGuSelected = { gu -> + selectedGu = gu + selectedDong = "" + }, + onDongSelected = { dong -> + selectedDong = dong + onRegionSelected(selectedGu, selectedDong) + }, + modifier = Modifier + .weight(1f) + ) + } +} + +@Composable +private fun RegionSearchList( + guList: List, + dongList: List, + selectedGu: String, + selectedDong: String, + onGuSelected: (String) -> Unit, + onDongSelected: (String) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .padding(vertical = 8.dp), + ) { + LazyColumn( + modifier = Modifier + .weight(0.45f), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + items(guList) { gu -> + RegionItem( + name = gu, + isSelected = gu == selectedGu, + onClick = { + onGuSelected(gu) + } + ) + } + } + + VerticalDivider( + thickness = 1.dp, + color = PawKeyTheme.colors.default, + modifier = Modifier + .fillMaxHeight() + .background( + color = PawKeyTheme.colors.default, + shape = RoundedCornerShape(8.dp) + ) + ) + + LazyColumn( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.Start + ) { + items(dongList) { dong -> + RegionItem( + name = dong, + isSelected = dong == selectedDong, + onClick = { + onDongSelected(dong) + }, + textAlign = TextAlign.Start + ) + } + } + } +} + +@Composable +private fun RegionItem( + name: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + textAlign: TextAlign = TextAlign.Center +) { + val textColor = if (isSelected) PawKeyTheme.colors.background2 else PawKeyTheme.colors.contents + + val backgroundColor = + if (isSelected) PawKeyTheme.colors.primary else PawKeyTheme.colors.background2 + + Text( + text = name, + style = PawKeyTheme.typography.bodyActive, + color = textColor, + modifier = modifier + .fillMaxWidth() + .background( + color = backgroundColor, + shape = RoundedCornerShape(8.dp) + ) + .padding( + horizontal = 16.dp, + vertical = 11.dp + ) + .noRippleClickable { + onClick() + }, + textAlign = textAlign + ) +} + + +@Preview +@Composable +private fun RegionSearchContentPreview() { + PawKeyTheme { + RegionSearchContent( + selectedGu = "", + selectedDong = "", + onRegionSelected = { _, _ -> } + ) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpHeader.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpHeader.kt index f2a4c317..bfcb6abd 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpHeader.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpHeader.kt @@ -3,14 +3,12 @@ package com.paw.key.presentation.ui.signup.component import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -18,9 +16,14 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.paw.key.R import com.paw.key.core.designsystem.theme.PawKeyTheme +import com.paw.key.core.extension.noRippleClickable @Preview(showBackground = true) @Composable @@ -29,7 +32,7 @@ private fun PreviewSignUpHeader() { SignUpHeader( progress = 0.5F, title = "회원가입", - subtitle = "견주님에 대해 알려주세요." + onBackClick = {} ) } } @@ -37,13 +40,14 @@ private fun PreviewSignUpHeader() { @Composable fun SignUpHeader( title: String, - subtitle: String, + onBackClick: () -> Unit, modifier: Modifier = Modifier, - progress: Float = 1F, + progress: Float = 1f, ) { + val stepProgress = (progress / 3f).coerceIn(0f, 1f) val animatedProgress by animateFloatAsState( - targetValue = progress, + targetValue = stepProgress, animationSpec = tween( durationMillis = 1000, easing = FastOutSlowInEasing @@ -51,54 +55,43 @@ fun SignUpHeader( label = "progress_animation" ) - Column( + Column ( modifier = modifier .fillMaxWidth() - .height(131.dp) - ) { - Row( - verticalAlignment = Alignment.Bottom, - modifier = modifier + ){ + Box ( + modifier = Modifier .fillMaxWidth() - .height(60.dp) + .padding(vertical = 16.dp), + contentAlignment = Alignment.Center ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_left_black), + contentDescription = "back", + tint = PawKeyTheme.colors.contents, modifier = Modifier - .weight(1F) - .fillMaxSize() - ) { - Text( - text = title, - color = PawKeyTheme.colors.black, - style = PawKeyTheme.typography.body16Sb, - modifier = Modifier - .padding(top = 16.dp), - ) - - Spacer(modifier = Modifier.weight(1F)) + .align(Alignment.CenterStart) + .padding(start = 16.dp) + .noRippleClickable(onBackClick) + ) - LinearProgressIndicator( - progress = { animatedProgress }, - modifier = Modifier - .fillMaxWidth() - .height(2.dp), - color = PawKeyTheme.colors.green500, - trackColor = PawKeyTheme.colors.gray100, - strokeCap = StrokeCap.Square, - gapSize = 0.dp, - drawStopIndicator = {} - ) - } + Text( + text = title, + color = PawKeyTheme.colors.contents, + style = PawKeyTheme.typography.subTitle, + textAlign = TextAlign.Center + ) } - Text( - text = subtitle, - color = PawKeyTheme.colors.black, - style = PawKeyTheme.typography.head22Sb, + + LinearProgressIndicator( + progress = { animatedProgress }, modifier = Modifier - .padding(top = 36.dp) - .padding(horizontal = 16.dp) + .fillMaxWidth() + .height(2.dp), + color = PawKeyTheme.colors.primary, + trackColor = PawKeyTheme.colors.default, + strokeCap = StrokeCap.Square, + gapSize = 0.dp ) } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpPetImageHolder.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpPetImageHolder.kt new file mode 100644 index 00000000..16c5a726 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpPetImageHolder.kt @@ -0,0 +1,68 @@ +package com.paw.key.presentation.ui.signup.component + +import android.net.Uri +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +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.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.paw.key.R +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@Composable +fun SignUpPetImageHolder( + uri: Uri?, + modifier: Modifier = Modifier +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .size(94.dp) + ) { + if (uri == null) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_signup_profile_edit), + contentDescription = "add pet profile", + tint = Color.Unspecified, + ) + } else { + AsyncImage( + model = uri, + contentDescription = "Pet Image", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + ) + + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_signup_image_edit), + contentDescription = "edit icon", + tint = Color.Unspecified, + modifier = Modifier + .align(Alignment.BottomEnd) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SignUpPetImageHolderPreview() { + PawKeyTheme { + SignUpPetImageHolder( + uri = null, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpSubHeader.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpSubHeader.kt new file mode 100644 index 00000000..06639c38 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpSubHeader.kt @@ -0,0 +1,44 @@ +package com.paw.key.presentation.ui.signup.component + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.paw.key.core.designsystem.theme.PawKeyTheme + +@Composable +fun SignUpSubHeader() { + Text( + text = "산책하기 전 \n간단한 정보를 입력해주세요", + color = PawKeyTheme.colors.contents, + style = PawKeyTheme.typography.header2, + modifier = Modifier + .padding( + top = 20.dp, + start = 16.dp, + end = 16.dp + ) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "서비스 시작을 위해 간단한 정보를 입력해주세요!", + color = PawKeyTheme.colors.default, + style = PawKeyTheme.typography.bodyDefault, + modifier = Modifier + .padding(horizontal = 16.dp) + ) +} + +@Preview +@Composable +private fun SignUpSubHeaderPreview() { + PawKeyTheme { + SignUpSubHeader() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt index acb1ac65..5c945cf3 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpTextField.kt @@ -2,81 +2,120 @@ package com.paw.key.presentation.ui.signup.component import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme @Composable + fun SignUpTextField( value: String, onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, placeholder: String, + modifier: Modifier = Modifier, enabled: Boolean = true, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, + visualTransformation: VisualTransformation = VisualTransformation.None, singleLine: Boolean = true, + suffix: @Composable (() -> Unit)? = null ) { val isFocused = remember { mutableStateOf(false) } val borderColor = when { - !enabled -> PawKeyTheme.colors.gray100 - isFocused.value -> PawKeyTheme.colors.green500 - else -> PawKeyTheme.colors.gray200 + !enabled -> PawKeyTheme.colors.default + isFocused.value -> PawKeyTheme.colors.primary + else -> PawKeyTheme.colors.default } - BasicTextField( - value = value, - onValueChange = onValueChange, - modifier = modifier - .fillMaxWidth() - .height(52.dp) - .clip(RoundedCornerShape(8.dp)) - .onFocusChanged { focusState -> - isFocused.value = focusState.isFocused - } - .background(color = PawKeyTheme.colors.white1) - .border( - width = 1.dp, - color = borderColor, - shape = RoundedCornerShape(8.dp) - ), - textStyle = PawKeyTheme.typography.body14R, - maxLines = if (singleLine) 1 else Int.MAX_VALUE, - singleLine = singleLine, - enabled = enabled, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - decorationBox = { innerTextField -> - androidx.compose.foundation.layout.Box( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .padding(horizontal = 16.dp), - contentAlignment = androidx.compose.ui.Alignment.CenterStart - ) { - if (value.isEmpty()) { - Text( - text = placeholder, - style = PawKeyTheme.typography.body14R, - color = PawKeyTheme.colors.gray200 - ) + // Todo : Gra로 변경 + val customTextSelectionColors = TextSelectionColors( + handleColor = PawKeyTheme.colors.primary, + backgroundColor = PawKeyTheme.colors.primary.copy(alpha = 0.4f) + ) + + CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) { + BasicTextField( + value = value, + onValueChange = onValueChange, + visualTransformation = visualTransformation, + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .onFocusChanged { focusState -> + isFocused.value = focusState.isFocused + } + .background(color = PawKeyTheme.colors.background2) + .border( + width = 1.dp, + color = borderColor, + shape = RoundedCornerShape(8.dp) + ), + textStyle = PawKeyTheme.typography.bodyActive, + maxLines = if (singleLine) 1 else Int.MAX_VALUE, + singleLine = singleLine, + enabled = enabled, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + cursorBrush = SolidColor(PawKeyTheme.colors.primary), + decorationBox = { innerTextField -> + Row ( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Box( + modifier = Modifier + .weight(1f) + ) { + if (value.isEmpty()) { + Text( + text = placeholder, + style = PawKeyTheme.typography.bodyDefault, + color = PawKeyTheme.colors.default + ) + } + innerTextField() + } + suffix?.invoke() } - innerTextField() } - } - ) + ) + } +} + +@Preview +@Composable +private fun SignUpTextFieldPreview() { + PawKeyTheme { + SignUpTextField( + value = "", + onValueChange = {}, + placeholder = "이름을 입력해주세요." + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpUserSelectButton.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpUserSelectButton.kt index 3b91fdf0..a39f92de 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpUserSelectButton.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/component/SignUpUserSelectButton.kt @@ -3,15 +3,14 @@ package com.paw.key.presentation.ui.signup.component import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box -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.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.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.paw.key.core.designsystem.theme.PawKeyTheme @@ -39,39 +38,41 @@ fun SignUpUserSelectButton( Box( contentAlignment = Alignment.Center, modifier = modifier - .height(48.dp) - .width(158.dp) - .background(color = PawKeyTheme.colors.white1) - .noRippleClickable { onClick() } - .border( - width = if (isSelect){ - 2.dp + .clip(RoundedCornerShape(8.dp)) + .background( + color = if (isSelect) { + PawKeyTheme.colors.primary } else { - 1.dp - }, + PawKeyTheme.colors.background2 + } + ) + .border( + width = 1.dp, color = if (isSelect) { - PawKeyTheme.colors.green500 + Color.Transparent } else { - PawKeyTheme.colors.gray200 + PawKeyTheme.colors.default }, shape = RoundedCornerShape(8.dp) ) - .clip(RoundedCornerShape(8.dp)) + .noRippleClickable(onClick) ) { Text( text = user, color = if (isSelect) { - PawKeyTheme.colors.green500 + PawKeyTheme.colors.background2 } else { - PawKeyTheme.colors.gray200 + PawKeyTheme.colors.default }, style = if (isSelect) { - PawKeyTheme.typography.body14Sb + PawKeyTheme.typography.bodyActive } else { - PawKeyTheme.typography.body14R + PawKeyTheme.typography.bodyDefault }, modifier = Modifier - .padding(horizontal = 21.dp, vertical = 13.dp) + .padding( + vertical = 16.dp + ) ) } } diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpLocationInfo.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpLocationInfo.kt new file mode 100644 index 00000000..1f0fa190 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpLocationInfo.kt @@ -0,0 +1,9 @@ +package com.paw.key.presentation.ui.signup.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class SignUpLocationInfo( // 바텀시트 지역 선택 시 보여주기 위함 + val selectedGu: String = "", + val selectedDong: String = "", +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpMapInfo.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpMapInfo.kt new file mode 100644 index 00000000..23273063 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpMapInfo.kt @@ -0,0 +1,16 @@ +package com.paw.key.presentation.ui.signup.model + +import androidx.compose.runtime.Immutable +import com.naver.maps.geometry.LatLng +import com.paw.key.core.util.UiState +import com.paw.key.presentation.ui.region.state.DrawType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +data class SignUpMapInfo( + val uiState: UiState>> = UiState.Loading, + val entireCoordinates: ImmutableList = persistentListOf(), + val regionName: String = "", + val drawType: DrawType = DrawType.SINGLE +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpPetInfo.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpPetInfo.kt new file mode 100644 index 00000000..8e81a8dd --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpPetInfo.kt @@ -0,0 +1,15 @@ +package com.paw.key.presentation.ui.signup.model + +import android.net.Uri +import androidx.compose.runtime.Immutable +import com.paw.key.presentation.ui.signup.state.Gender + +@Immutable +data class SignUpPetInfo( + val petImage : Uri? = null, + val petName : String = "", + val petBirthDate : String = "", + val petGender : Gender = Gender.UNKNOWN, + val petNeutered : Boolean = false, + val petBreed : String = "", +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpUserInfo.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpUserInfo.kt new file mode 100644 index 00000000..f103f0b5 --- /dev/null +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/model/SignUpUserInfo.kt @@ -0,0 +1,11 @@ +package com.paw.key.presentation.ui.signup.model + +import androidx.compose.runtime.Immutable +import com.paw.key.presentation.ui.signup.state.Gender + +@Immutable +data class SignUpUserInfo( + val nickName : String = "", + val birthDate : String = "", + val gender : Gender = Gender.UNKNOWN, +) \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/navigation/SignUpNavigation.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/navigation/SignUpNavigation.kt index 841628f8..6542affe 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/navigation/SignUpNavigation.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/navigation/SignUpNavigation.kt @@ -1,113 +1,29 @@ package com.paw.key.presentation.ui.signup.navigation -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import androidx.navigation.navigation import com.paw.key.core.navigation.Route -import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.presentation.ui.signup.SignUpActivityRoute -import com.paw.key.presentation.ui.signup.SignUpDogRoute -import com.paw.key.presentation.ui.signup.SignUpLevelRoute import com.paw.key.presentation.ui.signup.SignUpRoute -import com.paw.key.presentation.ui.signup.viewmodel.SignUpViewModel import kotlinx.serialization.Serializable -@Serializable data object SignUpFlow : Route -@Serializable data object SignUp : Route -@Serializable data object SignUpActivity : Route -@Serializable data object SignUpDog : Route -@Serializable data object SignUpLevel : Route - - -fun NavHostController.navigateSignUpFlow(navOptions: NavOptions? = null) { - this.navigate(SignUpFlow, navOptions) +fun NavHostController.navigateSignUp( + navOptions: NavOptions? = null +) { + navigate(SignUp, navOptions) } -@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) fun NavGraphBuilder.signUpNavGraph( - navController: NavHostController, navigateToHome: () -> Unit, + navigateUp: () -> Unit ) { - navigation( - startDestination = SignUp - ) { - composable { backStackEntry -> - - val parentEntry = remember(backStackEntry) { - navController.getBackStackEntry() - } - val signUpViewModel: SignUpViewModel = hiltViewModel(parentEntry) - - val context = LocalContext.current - val loginInfo by PreferenceDataStore.getLoginInfo().collectAsState( - initial = PreferenceDataStore.LoginInfo("", "") - ) - - LaunchedEffect(loginInfo) { - if (loginInfo.email.isNotEmpty() && loginInfo.password.isNotEmpty()) { - signUpViewModel.setLoginCredentials( - email = loginInfo.email, - password = loginInfo.password - ) - } - } - - SignUpRoute( - navigateSignUpActivity = { - navController.navigate(SignUpActivity) - }, - viewModel = signUpViewModel - ) - } - - composable { backStackEntry -> - val parentEntry = remember(backStackEntry) { - navController.getBackStackEntry() - } - val signUpViewModel: SignUpViewModel = hiltViewModel(parentEntry) - - SignUpActivityRoute( - navigateSignUpDog = { - navController.navigate(SignUpDog) - }, - viewModel = signUpViewModel - ) - } - - composable { backStackEntry -> - val parentEntry = remember(backStackEntry) { - navController.getBackStackEntry() - } - val signUpViewModel: SignUpViewModel = hiltViewModel(parentEntry) - - SignUpDogRoute( - navigateNext = { - navController.navigate(SignUpLevel) - }, - viewModel = signUpViewModel - ) - } - - composable { backStackEntry -> - val parentEntry = remember(backStackEntry) { - navController.getBackStackEntry() - } - val signUpViewModel: SignUpViewModel = hiltViewModel(parentEntry) - - SignUpLevelRoute( - navigateNext = navigateToHome, - viewModel = signUpViewModel - ) - } + composable { + SignUpRoute( + navigateUp = navigateUp, + navigateToHome = navigateToHome + ) } } + +@Serializable data object SignUp : Route \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/state/SignUpContract.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/state/SignUpContract.kt index ec2b09db..0767f40b 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/state/SignUpContract.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/state/SignUpContract.kt @@ -1,63 +1,39 @@ package com.paw.key.presentation.ui.signup.state -import android.net.Uri import androidx.compose.runtime.Immutable -import com.paw.key.data.dto.response.PetTraitCategoryDto - -class SignUpContract { - @Immutable - data class SignUpState( - val selectedGender: Gender = Gender.UNKNOWN, - val selectedLocation: String = "", - val isLocationMenuVisible: Boolean = false, - val name: String = "", - val age: String = "", - val dogImage: Uri? = null, - - val dogName: String = "", - val dogGender: DogGender = DogGender.UNKNOWN, - val isNeutered: Boolean = false, - val dogBreed: String = "", - val ageKnown: AgeKnown = AgeKnown.NONE, - val dogAge: String = "", - - val selectedDongId: Int = 0, - val selectedGuId: Int = 0, - - val selectedEnergyLevel: String = "", - val selectedSocialLevel: String = "", - - val selectedDistrict: List = emptyList(), - val isDistrictMenuVisible: Boolean = false, - - val petTraitCategoryList: List = emptyList(), - - val selectedGu: String = "", - val selectedDong: String = "", - - ) - - enum class Gender { - MALE, - FEMALE, - UNKNOWN - } - - enum class DogGender { - MALE, - FEMALE, - UNKNOWN - } +import com.paw.key.presentation.ui.signup.model.SignUpLocationInfo +import com.paw.key.presentation.ui.signup.model.SignUpMapInfo +import com.paw.key.presentation.ui.signup.model.SignUpPetInfo +import com.paw.key.presentation.ui.signup.model.SignUpUserInfo + +@Immutable +data class SignUpState( + val userInfo: SignUpUserInfo = SignUpUserInfo(), + val petInfo: SignUpPetInfo = SignUpPetInfo(), + val locationInfo: SignUpLocationInfo = SignUpLocationInfo(), + val mapInfo: SignUpMapInfo = SignUpMapInfo(), + val signUpState: SignUpStateType = SignUpStateType.USER_INFO, + val currentStep: Float = 1f, + val isNextEnabled: Boolean = false, + val isRegionComplete: Boolean = false, +) + +sealed class SignUpSideEffect { + data class ShowSnackBar(val message: String) : SignUpSideEffect() + data object NavigateUp : SignUpSideEffect() + data object NavigateNext : SignUpSideEffect() + data object NavigateHome : SignUpSideEffect() +} - enum class AgeKnown { - NONE, - KNOWN, - UNKNOWN - } +enum class SignUpStateType { + USER_INFO, + PET_INFO, + LOCATION_INFO, + REGION_MANAGEMENT, +} - sealed class SignUpSideEffect { - data class ShowSnackBar(val message: String) : SignUpSideEffect() - data object NavigateUp: SignUpSideEffect() - data object NavigateNext: SignUpSideEffect() - } +enum class Gender { + MALE, + FEMALE, + UNKNOWN } diff --git a/app/src/main/java/com/paw/key/presentation/ui/signup/viewmodel/SignUpViewModel.kt b/app/src/main/java/com/paw/key/presentation/ui/signup/viewmodel/SignUpViewModel.kt index 01e3b118..8a9bd399 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/signup/viewmodel/SignUpViewModel.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/signup/viewmodel/SignUpViewModel.kt @@ -1,486 +1,421 @@ package com.paw.key.presentation.ui.signup.viewmodel -import DistrictDto -import android.content.Context +import android.content.ContentResolver import android.net.Uri -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.paw.key.core.util.PreferenceDataStore -import com.paw.key.data.dto.request.onboarding.OnboardingInfoRequest -import com.paw.key.data.dto.request.onboarding.PetInfoDto -import com.paw.key.data.dto.request.onboarding.PetTraitDto +import com.paw.key.core.util.UiState +import com.paw.key.core.util.flattenCoordinatesToLatLng +import com.paw.key.core.util.handleError +import com.paw.key.domain.repository.RegionRepository import com.paw.key.domain.repository.onboarding.OnboardingInfoRepository import com.paw.key.domain.repository.onboarding.OnboardingRegionRepository import com.paw.key.domain.repository.onboarding.OnboardingRepository -import com.paw.key.presentation.ui.signup.state.SignUpContract +import com.paw.key.presentation.ui.region.state.DrawType +import com.paw.key.presentation.ui.signup.state.Gender +import com.paw.key.presentation.ui.signup.state.SignUpSideEffect +import com.paw.key.presentation.ui.signup.state.SignUpState +import com.paw.key.presentation.ui.signup.state.SignUpStateType import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.asRequestBody -import java.io.File +import timber.log.Timber +import java.time.LocalDate +import java.time.format.DateTimeFormatter import javax.inject.Inject @HiltViewModel class SignUpViewModel @Inject constructor( + private val contentResolver : ContentResolver, + private val locationRegionRepository: RegionRepository, private val repository: OnboardingRepository, private val regionRepository: OnboardingRegionRepository, private val infoRepository: OnboardingInfoRepository, ) : ViewModel() { + private val _state = MutableStateFlow(SignUpState()) + val state: StateFlow = _state.asStateFlow() - private val _state = MutableStateFlow(SignUpContract.SignUpState()) - val state: StateFlow = _state.asStateFlow() + private val _sideEffect = MutableSharedFlow() + val sideEffect: MutableSharedFlow = _sideEffect - private val _sideEffect = MutableSharedFlow() - val sideEffect: MutableSharedFlow = _sideEffect - - private val _regionList = MutableStateFlow>(emptyList()) - val regionList: StateFlow> = _regionList.asStateFlow() - - private var loginEmail: String = "" - private var loginPassword: String = "" - - private val userId = PreferenceDataStore.getUserId() + private val userId : StateFlow = PreferenceDataStore.getUserId() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = -1 + ) - init { - fetchPetTraits() - fetchRegion() + fun deniedPermission() { + viewModelScope.launch { + _sideEffect.emit(SignUpSideEffect.ShowSnackBar("갤러리 접근 권한을 허용해주세요")) + } } - fun selectGender(gender: SignUpContract.Gender) { - _state.update { it.copy(selectedGender = gender) } - } + fun onBackPressed() { + viewModelScope.launch { + Timber.e("onBackPressed ${_state.value.currentStep}") + when (state.value.signUpState) { + SignUpStateType.USER_INFO -> { + _sideEffect.emit(SignUpSideEffect.NavigateUp) + } - fun onDogImageSelected(uri: Uri) { - _state.update { it.copy(dogImage = uri) } - } + SignUpStateType.PET_INFO -> { + updateState { + it.copy( + signUpState = SignUpStateType.USER_INFO, + currentStep = it.currentStep - 1f + ) + } + } - fun selectDistrict(districts: List) { - _state.update { it.copy(selectedDistrict = districts) } - } + SignUpStateType.LOCATION_INFO -> { + updateState { + it.copy( + signUpState = SignUpStateType.PET_INFO, + currentStep = it.currentStep - 1f + ) + } + } - fun selectLocation(location: String) { - _state.update { it.copy(selectedLocation = location) } + SignUpStateType.REGION_MANAGEMENT -> { + updateState { currentState -> + currentState.copy( + signUpState = SignUpStateType.LOCATION_INFO, + isRegionComplete = false, + locationInfo = currentState.locationInfo.copy( + selectedGu = "", + selectedDong = "" + ) + ) + } + } + } + } } - fun onLocationChanged(location: String) { - _state.update { it.copy(selectedLocation = location) } - } + fun onNextClick() { + viewModelScope.launch { + if (!validateCurrentStep(_state.value)) { + _state.update { currentState -> + currentState.copy( + isNextEnabled = false + ) + } + _sideEffect.emit(SignUpSideEffect.ShowSnackBar("입력되지 않은 정보가 있습니다.")) + return@launch + } - fun toggleLocationMenu() { - _state.update { it.copy(isLocationMenuVisible = !_state.value.isLocationMenuVisible) } - } + when (_state.value.signUpState) { + SignUpStateType.USER_INFO -> { + updateState { + it.copy( + signUpState = SignUpStateType.PET_INFO, + ) + } + _sideEffect.emit(SignUpSideEffect.NavigateNext) + } - fun onNameChanged(name: String) { - _state.update { it.copy(name = name) } - } + SignUpStateType.PET_INFO -> { + updateState { + it.copy( + signUpState = SignUpStateType.LOCATION_INFO, + ) + } + _sideEffect.emit(SignUpSideEffect.NavigateNext) + } - fun onAgeChanged(age: String) { - _state.update { it.copy(age = age) } - } + SignUpStateType.LOCATION_INFO -> { + // TODO: 서버에 값 전송 후 Home으로 + //submitRegistration() + // Todo : 서버 전송 시 uri를 contentresolver로 실제 사진 가져와서 전송하기 그리고 꼭 close하기 + //val inputStream = contentResolver.openInputStream(_state.value.petInfo.petImage) + if (_state.value.isRegionComplete && _state.value.locationInfo.selectedGu.isNotBlank() && _state.value.locationInfo.selectedDong.isNotBlank()) { + _sideEffect.emit(SignUpSideEffect.NavigateHome) + } else { + updateState { + it.copy( + signUpState = SignUpStateType.REGION_MANAGEMENT, + ) + } + _sideEffect.emit(SignUpSideEffect.NavigateNext) + } + } - fun hideLocationMenu() { - _state.update { it.copy(isLocationMenuVisible = false) } + SignUpStateType.REGION_MANAGEMENT -> { + updateState { + it.copy( + signUpState = SignUpStateType.LOCATION_INFO, + isRegionComplete = true + ) + } + } + } + } } - fun onGuSelected(guName: String, guId: Int) { + private fun updateState(updateAction: (SignUpState) -> SignUpState) { _state.update { currentState -> - currentState.copy( - selectedGu = guName, - selectedGuId = guId, - // 구를 새로 선택하면 기존 동 선택 초기화 - selectedDong = "", - selectedDongId = 0, - // 구 선택 후 메뉴 닫기 - isLocationMenuVisible = false - ) + val newState = updateAction(currentState) + val isButtonEnabled = validateCurrentStep(newState) + newState.copy(isNextEnabled = isButtonEnabled) } } - fun onDongSelected(dongName: String, dongId: Int) { - _state.update { it.copy(selectedDong = dongName, selectedDongId = dongId) } - } - - fun onDogNameChanged(dogName: String) { - _state.update { it.copy(dogName = dogName) } - } - - fun selectDogGender(dogGender: SignUpContract.DogGender) { - _state.update { it.copy(dogGender = dogGender) } - } - - fun toggleNeutering() { - _state.update { it.copy(isNeutered = !_state.value.isNeutered) } - } - - fun onDogBreedChanged(dogBreed: String) { - _state.update { it.copy(dogBreed = dogBreed) } - } - - fun selectAgeKnown(ageKnown: SignUpContract.AgeKnown) { - _state.update { it.copy(ageKnown = ageKnown) } + fun updateStep() { + viewModelScope.launch { + _state.update { currentState -> + if (_state.value.signUpState != SignUpStateType.REGION_MANAGEMENT) { + currentState.copy( + currentStep = _state.value.currentStep + 1f + ) + } else { + currentState.copy( + currentStep = _state.value.currentStep + ) + } + } + } } - fun onDogAgeChanged(dogAge: String) { - _state.update { it.copy(dogAge = dogAge) } + // userinfo + fun updateNickname(nickname: String) { + viewModelScope.launch { + updateState { currentState -> + currentState.copy( + userInfo = currentState.userInfo.copy(nickName = nickname) + ) + } + } } - fun selectEnergyLevel(energyLevel: String) { - Log.d("SignUpViewModel", "Energy level selected: $energyLevel") - _state.update { it.copy(selectedEnergyLevel = energyLevel) } - } + fun updateBirthDate(birthDate: String) { + val digitsOnly = birthDate.filter { it.isDigit() } - fun selectSocialLevel(socialLevel: String) { - Log.d("SignUpViewModel", "Social level selected: $socialLevel") - _state.update { it.copy(selectedSocialLevel = socialLevel) } + if (digitsOnly.length <= 8) { + updateState { currentState -> + currentState.copy( + userInfo = currentState.userInfo.copy(birthDate = digitsOnly) + ) + } + } } - fun isNextButtonEnabled(): Boolean { - return isSignUpEnabled() + fun updateGender(gender: Gender) { + viewModelScope.launch { + updateState { currentState -> + currentState.copy( + userInfo = currentState.userInfo.copy(gender = gender) + ) + } + } } - fun isLevelScreenEnabled(): Boolean { - val state = _state.value - return state.selectedEnergyLevel.isNotEmpty() && - state.selectedSocialLevel.isNotEmpty() + // petinfo + fun updatePetName(name: String) { + viewModelScope.launch { + updateState { currentState -> + currentState.copy( + petInfo = currentState.petInfo.copy(petName = name) + ) + } + } } - fun isSignUpEnabled(): Boolean { - val state = _state.value - Log.d( - "SignUpViewModel", """ - isSignUpEnabled check: - - selectedEnergyLevel: '${state.selectedEnergyLevel}' (isEmpty: ${state.selectedEnergyLevel.isEmpty()}) - - selectedSocialLevel: '${state.selectedSocialLevel}' (isEmpty: ${state.selectedSocialLevel.isEmpty()}) - - name: '${state.name}' (isEmpty: ${state.name.isEmpty()}) - - age: '${state.age}' (isEmpty: ${state.age.isEmpty()}) - - selectedGu: '${state.selectedGu}' (isEmpty: ${state.selectedGu.isEmpty()}) - - selectedDong: '${state.selectedDong}' (isEmpty: ${state.selectedDong.isEmpty()}) - - dogName: '${state.dogName}' (isEmpty: ${state.dogName.isEmpty()}) - - dogBreed: '${state.dogBreed}' (isEmpty: ${state.dogBreed.isEmpty()}) - - dogImage: ${state.dogImage != null} - - loginEmail: '$loginEmail' (isEmpty: ${loginEmail.isEmpty()}) - - loginPassword: '$loginPassword' (isEmpty: ${loginPassword.isEmpty()}) - """.trimIndent() - ) - - return state.selectedEnergyLevel.isNotEmpty() && - state.selectedSocialLevel.isNotEmpty() && - state.name.isNotEmpty() && - state.age.isNotEmpty() && - state.selectedGu.isNotEmpty() && - state.selectedDong.isNotEmpty() && - state.dogName.isNotEmpty() && - state.dogBreed.isNotEmpty() && - state.dogImage != null && - loginEmail.isNotEmpty() && - loginPassword.isNotEmpty() - } + fun updatePetBirthDate(birthDate: String) { + val digitsOnly = birthDate.filter { it.isDigit() } - fun setLoginCredentials(email: String, password: String) { - loginEmail = email - loginPassword = password - Log.d( - "SignUpViewModel", - "Login credentials set - Email: '$email', Password length: ${password.length}" - ) + if (digitsOnly.length <= 8) { + updateState { currentState -> + currentState.copy( + petInfo = currentState.petInfo.copy(petBirthDate = digitsOnly) + ) + } + } } - fun debugSignUpState() { - val state = _state.value - Log.d( - "DEBUG_SIGNUP", """ - === SignUp State Debug === - Energy: '${state.selectedEnergyLevel}' (empty: ${state.selectedEnergyLevel.isEmpty()}) - Social: '${state.selectedSocialLevel}' (empty: ${state.selectedSocialLevel.isEmpty()}) - Name: '${state.name}' (empty: ${state.name.isEmpty()}) - Age: '${state.age}' (empty: ${state.age.isEmpty()}) - Gu: '${state.selectedGu}' (empty: ${state.selectedGu.isEmpty()}) - GuId: ${state.selectedGuId} - Dong: '${state.selectedDong}' (empty: ${state.selectedDong.isEmpty()}) - DongId: ${state.selectedDongId} - Dog Name: '${state.dogName}' (empty: ${state.dogName.isEmpty()}) - Dog Breed: '${state.dogBreed}' (empty: ${state.dogBreed.isEmpty()}) - Dog Image: ${state.dogImage != null} - Login Email: '$loginEmail' (empty: ${loginEmail.isEmpty()}) - Login Password: '$loginPassword' (length: ${loginPassword.length}) - - 각 단계별 체크: - - 성향 정보: ${state.selectedEnergyLevel.isNotEmpty() && state.selectedSocialLevel.isNotEmpty()} - - 개인 정보: ${state.name.isNotEmpty() && state.age.isNotEmpty()} - - 지역 정보: ${state.selectedGu.isNotEmpty() && state.selectedDong.isNotEmpty()} - - 반려견 정보: ${state.dogName.isNotEmpty() && state.dogBreed.isNotEmpty() && state.dogImage != null} - - 로그인 정보: ${loginEmail.isNotEmpty() && loginPassword.isNotEmpty()} - - 최종 가능 여부: ${isSignUpEnabled()} - ========================= - """.trimIndent() - ) + fun updatePetGender(gender: Gender) { + viewModelScope.launch { + updateState { currentState -> + currentState.copy( + petInfo = currentState.petInfo.copy(petGender = gender) + ) + } + } } - private fun fetchPetTraits() { + fun updatePetNeutered(neutered: Boolean) { viewModelScope.launch { - try { - val result = repository.getOnboardingPets(userId = userId.first()) - result.onSuccess { response -> - Log.d( - "SignUpViewModel", - "Pet traits loaded: ${response.data.petTraitCategoryList.size}" - ) - _state.update { it.copy(petTraitCategoryList = response.data.petTraitCategoryList) } - }.onFailure { error -> - Log.e("SignUpViewModel", "성향 정보 불러오기 실패: ${error.message}") - } - } catch (e: Exception) { - Log.e("SignUpViewModel", "fetchPetTraits Exception: ${e.message}") + updateState { currentState -> + currentState.copy( + petInfo = currentState.petInfo.copy(petNeutered = neutered) + ) } } } - private fun getSelectedRegionId(): Int { - val state = _state.value - - // selectedDongId가 있으면 그것을 우선 사용 - if (state.selectedDongId != 0) { - Log.d("SignUpViewModel", "Using selectedDongId: ${state.selectedDongId}") - return state.selectedDongId + fun updatePetBreed(breed: String) { + viewModelScope.launch { + updateState { currentState -> + currentState.copy( + petInfo = currentState.petInfo.copy(petBreed = breed) + ) + } } + } - // 없으면 기존 방식으로 찾기 - val selectedDong = _regionList.value - .find { it.gu.name == state.selectedGu } - ?.dongs - ?.find { it.name == state.selectedDong } - - Log.d( - "SignUpViewModel", - "Selected region - Gu: ${state.selectedGu}, Dong: ${state.selectedDong}, DongId: ${selectedDong?.id}" - ) - return selectedDong?.id ?: run { - Log.e( - "SignUpViewModel", - "동 ID를 찾을 수 없습니다. Gu: ${state.selectedGu}, Dong: ${state.selectedDong}" - ) - 1 + fun updatePetImage(uri: Uri?) { + viewModelScope.launch { + updateState { currentState -> + currentState.copy( + petInfo = currentState.petInfo.copy(petImage = uri) + ) + } } } - fun fetchRegion() { - viewModelScope.launch { - try { - val result = regionRepository.getOnboardingRegion(userId = userId.first()) - result.onSuccess { response -> - Log.d("SignUpViewModel", "Region loaded: ${response.data.districtDtos.size}") - _regionList.value = response.data.districtDtos - }.onFailure { - Log.e("SignUpViewModel", "구/동 가져오기 실패: ${it.message}") - } - } catch (e: Exception) { - Log.e("SignUpViewModel", "fetchRegion Exception: ${e.message}") + // location + fun onRegionSelected(gu: String, dong: String) { + updateState { currentState -> + // 원래 선택한 구와 동이 같으면 완료로 아니라면 다시 지도뷰 + if (gu != currentState.locationInfo.selectedGu || dong != currentState.locationInfo.selectedDong) { + currentState.copy( + locationInfo = currentState.locationInfo.copy( + selectedGu = gu, + selectedDong = dong, + ), + isRegionComplete = false, + signUpState = SignUpStateType.LOCATION_INFO + ) + } else { + currentState.copy( + locationInfo = currentState.locationInfo.copy( + selectedGu = gu, + selectedDong = dong, + ), + signUpState = SignUpStateType.LOCATION_INFO + ) } } + getRegion() } - fun signUp(context: Context) { + fun getRegion() { viewModelScope.launch { - try { - Log.d("SignUpViewModel", "Starting signUp process...") - val state = _state.value - - // 에너지 레벨과 사회성 레벨 체크 - if (state.selectedEnergyLevel.isEmpty() || state.selectedSocialLevel.isEmpty()) { - Log.e("SignUpViewModel", "Energy or social level not selected") - _sideEffect.emit(SignUpContract.SignUpSideEffect.ShowSnackBar("에너지 레벨과 사회성 레벨을 모두 선택해주세요.")) - return@launch - } - - // 지역 정보 체크 - if (state.selectedGu.isEmpty() || state.selectedDong.isEmpty()) { - Log.e("SignUpViewModel", "Region not selected") - _sideEffect.emit(SignUpContract.SignUpSideEffect.ShowSnackBar("지역을 선택해주세요.")) - return@launch - } - - val regionId = getSelectedRegionId() - if (regionId == 1) { // 기본값이면 실제로 선택되지 않았을 가능성 - Log.e("SignUpViewModel", "Invalid region ID") - _sideEffect.emit(SignUpContract.SignUpSideEffect.ShowSnackBar("올바른 지역을 선택해주세요.")) - return@launch - } - - // 전체 회원가입 정보 체크 - if (!isSignUpEnabled()) { - Log.e("SignUpViewModel", "Required signup info missing") - _sideEffect.emit(SignUpContract.SignUpSideEffect.ShowSnackBar("회원가입 정보가 부족합니다.")) - return@launch - } - - // 나이 유효성 체크 - val userAge = state.age.toIntOrNull() - if (userAge == null || userAge <= 0) { - Log.e("SignUpViewModel", "Invalid user age: ${state.age}") - _sideEffect.emit(SignUpContract.SignUpSideEffect.ShowSnackBar("올바른 나이를 입력해주세요.")) - return@launch - } + val validUserId = userId.filter { it != -1 }.first() + getRegionGeometry( + userId = validUserId, + regionId = 39, + ) + onNextClick() + } + } - // 강아지 나이 유효성 체크 (나이를 안다고 했을 때만) - val dogAge = if (state.ageKnown == SignUpContract.AgeKnown.KNOWN) { - state.dogAge.toIntOrNull()?.takeIf { it >= 0 } ?: run { - Log.e("SignUpViewModel", "Invalid dog age: ${state.dogAge}") - _sideEffect.emit(SignUpContract.SignUpSideEffect.ShowSnackBar("올바른 강아지 나이를 입력해주세요.")) - return@launch + fun getRegionGeometry(userId: Int, regionId: Int?) = viewModelScope.launch { + locationRegionRepository.getRegionGeometry(userId, regionId!!) + .onSuccess { data -> + val coordinates = data.geometry.coordinates + val flattenedLatLng = flattenCoordinatesToLatLng(coordinates) + + if (flattenedLatLng.isEmpty() || flattenedLatLng.first().isEmpty()) { + _state.update { currentState -> + currentState.copy( + mapInfo = currentState.mapInfo.copy( + uiState = UiState.Failure("좌표 데이터가 올바르지 않습니다") + ) + ) } - } else { - 0 - } - - // trait ID 찾기 - val energyTraitId = state.petTraitCategoryList - .find { it.petTraitCategoryName == "에너지레벨" } - ?.petTraitCategoryOptions - ?.find { it.petTraitCategoryOptionText == state.selectedEnergyLevel } - ?.petTraitCategoryOptionId - - val socialTraitId = state.petTraitCategoryList - .find { it.petTraitCategoryName == "사회성레벨" } - ?.petTraitCategoryOptions - ?.find { it.petTraitCategoryOptionText == state.selectedSocialLevel } - ?.petTraitCategoryOptionId - - if (energyTraitId == null || socialTraitId == null) { - Log.e( - "SignUpViewModel", - "Trait ID not found - Energy: $energyTraitId, Social: $socialTraitId" - ) - _sideEffect.emit(SignUpContract.SignUpSideEffect.ShowSnackBar("성향 정보를 다시 선택해주세요.")) return@launch } - Log.d( - "SignUpViewModel", - "Energy trait ID: $energyTraitId, Social trait ID: $socialTraitId" - ) - - // 요청 객체 생성 - val request = OnboardingInfoRequest( - loginId = loginEmail, - password = loginPassword, - name = state.name, - gender = when (state.selectedGender) { - SignUpContract.Gender.MALE -> "M" - SignUpContract.Gender.FEMALE -> "F" - SignUpContract.Gender.UNKNOWN -> "M" - }, - age = userAge, - regionId = getSelectedRegionId(), - pet = PetInfoDto( - name = state.dogName, - gender = when (state.dogGender) { - SignUpContract.DogGender.MALE -> "M" - SignUpContract.DogGender.FEMALE -> "F" - SignUpContract.DogGender.UNKNOWN -> "M" // 예외.. 를 위해 일단 달아놨슴다 - }, - age = dogAge, - isAgeKnown = state.ageKnown == SignUpContract.AgeKnown.KNOWN, - isNeutered = state.isNeutered, - breed = state.dogBreed, - petTraits = listOf( - PetTraitDto( - traitCategoryId = 1, - traitOptionId = energyTraitId - ), - PetTraitDto( - traitCategoryId = 2, - traitOptionId = socialTraitId + val allPoints = flattenedLatLng.flatten().toPersistentList() + + if (flattenedLatLng.size == 1) { + // 폴리곤이 하나일 경우 + _state.update { currentState -> + currentState.copy( + mapInfo = currentState.mapInfo.copy( + uiState = UiState.Success(flattenedLatLng), + entireCoordinates = allPoints, + drawType = DrawType.SINGLE, + regionName = data.regionName ) ) - ) - ) - - // 이미지 처리 - val imagePart = state.dogImage?.let { uri -> - try { - val inputStream = context.contentResolver.openInputStream(uri) - val tempFile = File.createTempFile("pet_profile", ".jpg", context.cacheDir) - inputStream?.use { input -> - tempFile.outputStream().use { output -> input.copyTo(output) } - } - val fileRequestBody = tempFile.asRequestBody("image/jpeg".toMediaType()) - MultipartBody.Part.createFormData( - "pet_profile", - "pet_image.jpg", - fileRequestBody + } + } else { + // 폴리곤이 여러 개일 경우 + _state.update { currentState -> + currentState.copy( + mapInfo = currentState.mapInfo.copy( + uiState = UiState.Success(flattenedLatLng), + entireCoordinates = allPoints, + drawType = DrawType.MULTIPLE, + regionName = data.regionName + ) ) - } catch (e: Exception) { - Log.e("SignUpViewModel", "Image processing failed: ${e.message}") - null } } - - if (imagePart == null) { - _sideEffect.emit(SignUpContract.SignUpSideEffect.ShowSnackBar("이미지를 선택해주세요.")) - return@launch - } - - Log.d("SignUpViewModel", "Image processing successful ${userId.first()}") - // 서버 요청 - val result = infoRepository.postOnboardingInfo( - userId = userId.first(), - image = imagePart, - onboardingInfoRequest = request - ) - - result.onSuccess { response -> - Log.d("SignUpViewModel", "SignUp successful - Response: $response") - - // 회원가입 성공 시 사용자 정보를 DataStore에 저장 - try { - PreferenceDataStore.saveUserInfo( - userId = response.data.userId, - userName = response.data.userName, - petId = response.data.petId, - petName = response.data.petName + } + .onFailure { throwable -> + val errorMessage = handleError(throwable) + _state.update { currentState -> + currentState.copy( + mapInfo = currentState.mapInfo.copy( + uiState = UiState.Failure(errorMessage) ) + ) + } + } + } - // 로그인 정보도 함께 저장 - PreferenceDataStore.saveLoginInfo( - email = loginEmail, - password = loginPassword - ) + private fun validateCurrentStep(state: SignUpState): Boolean { + return when (state.signUpState) { + SignUpStateType.USER_INFO -> { + // UserInfo 확인 + state.userInfo.nickName.isNotBlank() && state.userInfo.nickName.length <= 8 && + state.userInfo.birthDate.length == 8 && state.userInfo.birthDate.isValidDate() && + state.userInfo.gender != Gender.UNKNOWN + } - Log.d( - "SignUpViewModel", - "User info and login info saved to DataStore successfully" - ) - Log.d( - "SignUpViewModel", - "Saved - UserId: ${response.data.userId}, UserName: ${response.data.userName}, PetId: ${response.data.petId}, PetName: ${response.data.petName}" - ) + SignUpStateType.PET_INFO -> { + // PetInfo 확인 + state.petInfo.petName.isNotBlank() && state.petInfo.petName.length <= 8 && + state.petInfo.petBirthDate.length == 8 && state.petInfo.petBirthDate.isValidDate() && + state.petInfo.petGender != Gender.UNKNOWN && + state.petInfo.petBreed.isNotBlank() && + state.petInfo.petImage != null + } - } catch (e: Exception) { - Log.e( - "SignUpViewModel", - "Failed to save user info to DataStore: ${e.message}" - ) - } + SignUpStateType.LOCATION_INFO -> { + // LocationInfo 확인 + state.locationInfo.selectedGu.isNotBlank() && + state.locationInfo.selectedDong.isNotBlank() + } - _sideEffect.emit(SignUpContract.SignUpSideEffect.NavigateNext) - }.onFailure { error -> - Log.e("SignUpViewModel", "SignUp failed: ${error.message}") - Log.e("SignUpViewModel", "SignUp failed: ${request}") - _sideEffect.emit(SignUpContract.SignUpSideEffect.ShowSnackBar("회원가입 실패: ${error.message}")) - } - } catch (e: Exception) { - Log.e("SignUpViewModel", "SignUp Exception: ${e.message}") - _sideEffect.emit(SignUpContract.SignUpSideEffect.ShowSnackBar("알 수 없는 오류가 발생했습니다.")) + SignUpStateType.REGION_MANAGEMENT -> { + state.locationInfo.selectedGu.isNotBlank() && + state.locationInfo.selectedDong.isNotBlank() } } } +} + +private fun String.isValidDate(): Boolean { + if (this.length != 8) return false + return try { + val formatter = DateTimeFormatter.ofPattern("yyyyMMdd") + LocalDate.parse(this, formatter) + true + } catch (e: Exception) { + false + } } \ No newline at end of file diff --git a/app/src/main/java/com/paw/key/presentation/ui/splash/SplashScreen.kt b/app/src/main/java/com/paw/key/presentation/ui/splash/SplashScreen.kt index 36e80d72..9f0d9101 100644 --- a/app/src/main/java/com/paw/key/presentation/ui/splash/SplashScreen.kt +++ b/app/src/main/java/com/paw/key/presentation/ui/splash/SplashScreen.kt @@ -1,23 +1,23 @@ package com.paw.key.presentation.ui.splash +import android.app.Activity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +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.toArgb import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -47,6 +47,22 @@ fun SplashRoute( viewModel: SplashViewModel = hiltViewModel(), ) { val effectFlow = viewModel.sideeffect + val statusBarColor = PawKeyTheme.colors.green500 + + val context = LocalContext.current + val window = (context as? Activity)?.window + val previousNavBarColor = remember { window?.statusBarColor } + + DisposableEffect(Unit) { + window?.statusBarColor = statusBarColor.toArgb() + + onDispose { + // 화면에서 벗어날 때 원래 색으로 복원 + previousNavBarColor?.let { + window?.statusBarColor = it + } + } + } LaunchedEffect(Unit) { effectFlow.collect { effect -> diff --git a/app/src/main/res/drawable/ic_roundcheck_invalid.xml b/app/src/main/res/drawable/ic_roundcheck_invalid.xml index 194239ca..c037335a 100644 --- a/app/src/main/res/drawable/ic_roundcheck_invalid.xml +++ b/app/src/main/res/drawable/ic_roundcheck_invalid.xml @@ -1,20 +1,11 @@ + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16"> - - + android:fillColor="#FFFFFFFF" + android:strokeColor="#FF9C9C9C" + android:strokeWidth="1" + android:pathData="M8 0.5A7.5 7.5 0 1 0 8 15.5 7.5 7.5 0 1 0 8 0.5z"/> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_roundcheck_valid.xml b/app/src/main/res/drawable/ic_roundcheck_valid.xml index b8fdbc78..e365437d 100644 --- a/app/src/main/res/drawable/ic_roundcheck_valid.xml +++ b/app/src/main/res/drawable/ic_roundcheck_valid.xml @@ -1,16 +1,14 @@ + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16"> + android:fillColor="#FFFFFFFF" + android:strokeColor="#FF00D281" + android:strokeWidth="1" + android:pathData="M8 0.5A7.5 7.5 0 1 0 8 15.5 7.5 7.5 0 1 0 8 0.5z"/> - + android:fillColor="#FF00D281" + android:pathData="M8 3A5 5 0 1 0 8 13 5 5 0 1 0 8 3z"/> + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_signup_image_edit.xml b/app/src/main/res/drawable/ic_signup_image_edit.xml new file mode 100644 index 00000000..7995ee96 --- /dev/null +++ b/app/src/main/res/drawable/ic_signup_image_edit.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_signup_profile_edit.xml b/app/src/main/res/drawable/ic_signup_profile_edit.xml new file mode 100644 index 00000000..3bab1219 --- /dev/null +++ b/app/src/main/res/drawable/ic_signup_profile_edit.xml @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_signup_search.xml b/app/src/main/res/drawable/ic_signup_search.xml new file mode 100644 index 00000000..f417e12c --- /dev/null +++ b/app/src/main/res/drawable/ic_signup_search.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 0957ce2d..6d25d34f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,5 @@ -