diff --git a/app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt b/app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt index aff7241a4..ec71bea3f 100644 --- a/app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt +++ b/app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt @@ -34,6 +34,8 @@ fun NavGraphBuilder.areaVerificationNavigation( }, skippable = LocalNavController.current.hasPreviousBackStackEntry().not(), onNavigateToChooseDislikes = { navController.navigateAndClear(OnboardingRoute.ChooseDislikes) }, + onNavigateToIntroduce = { navController.navigateAndClear(OnboardingRoute.Introduce) }, + onNavigateToSpotList = { navController.navigateAndClear(SpotRoute.SpotList) } ) } diff --git a/app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt b/app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt index 23777819e..08de06551 100644 --- a/app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt +++ b/app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt @@ -2,7 +2,6 @@ package com.acon.acon.navigation.nested import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.systemBarsPadding @@ -12,13 +11,12 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import com.acon.acon.core.designsystem.effect.screenDefault -import com.acon.acon.core.designsystem.theme.AconTheme import com.acon.acon.core.model.model.spot.SpotNavigationParameter import com.acon.acon.core.navigation.route.ProfileRoute import com.acon.acon.core.navigation.route.SettingsRoute import com.acon.acon.core.navigation.route.SpotRoute import com.acon.acon.core.navigation.route.UploadRoute -import com.acon.acon.feature.profile.composable.screen.bookmark.composable.BookmarkScreenContainer +import com.acon.feature.profile.savedspot.composable.BookmarkScreenContainer import com.acon.feature.profile.info.composable.ProfileInfoScreenContainer import com.acon.feature.profile.update.composable.ProfileUpdateScreenContainer diff --git a/app/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt b/app/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt index 5017f9f1f..60fafb0a6 100644 --- a/app/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt +++ b/app/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt @@ -14,6 +14,7 @@ import com.acon.acon.core.navigation.route.OnboardingRoute import com.acon.acon.core.navigation.route.ProfileRoute import com.acon.acon.core.navigation.route.SettingsRoute import com.acon.acon.core.navigation.route.SignInRoute +import com.acon.acon.core.navigation.utils.navigateAndClear import com.acon.acon.feature.settings.screen.composable.SettingsScreenContainer import com.acon.acon.feature.verification.screen.composable.UserVerifiedAreasScreenContainer import com.acon.acon.feature.withdraw.screen.composable.DeleteAccountScreenContainer @@ -44,11 +45,7 @@ internal fun NavGraphBuilder.settingsNavigation( navController.navigate(SettingsRoute.UserVerifiedAreas) }, onNavigateToSignInScreen = { - navController.navigate(SignInRoute.SignIn) { - popUpTo(SettingsRoute.Graph) { - inclusive = true - } - } + navController.navigateAndClear(SignInRoute.SignIn) }, onNavigateToDeleteAccountScreen = { navController.navigate(SettingsRoute.DeleteAccount) diff --git a/core/data/src/main/kotlin/com/acon/core/data/api/remote/ProfileApi.kt b/core/data/src/main/kotlin/com/acon/core/data/api/remote/ProfileApi.kt index 39742a085..99dc4ef40 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/api/remote/ProfileApi.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/api/remote/ProfileApi.kt @@ -1,12 +1,16 @@ package com.acon.core.data.api.remote +import com.acon.core.data.dto.request.ReplaceVerifiedAreaRequest import com.acon.core.data.dto.request.profile.UpdateProfileRequest +import com.acon.core.data.dto.response.area.VerifiedAreaListResponse import com.acon.core.data.dto.response.profile.ProfileResponse -import com.acon.core.data.dto.response.profile.SavedSpotResponse import com.acon.core.data.dto.response.profile.SavedSpotsResponse import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path import retrofit2.http.Query interface ProfileApi { @@ -22,4 +26,17 @@ interface ProfileApi { @GET("/api/v1/saved-spots") suspend fun getSavedSpots() : SavedSpotsResponse + + @GET("/api/v1/verified-areas") + suspend fun getVerifiedAreas() : VerifiedAreaListResponse + + @POST("/api/v1/verified-areas/replacement") + suspend fun replaceVerifiedArea( + @Body request: ReplaceVerifiedAreaRequest + ) + + @DELETE("/api/v1/verified-areas/{verifiedAreaId}") + suspend fun deleteVerifiedArea( + @Path("verifiedAreaId") verifiedAreaId: Long + ) } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/ProfileAuthApiLegacy.kt b/core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/ProfileAuthApiLegacy.kt deleted file mode 100644 index 895050423..000000000 --- a/core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/ProfileAuthApiLegacy.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.acon.core.data.api.remote.auth - -import com.acon.core.data.dto.request.ReplaceVerifiedAreaRequest -import com.acon.core.data.dto.request.SaveSpotRequest -import com.acon.core.data.dto.request.UpdateProfileRequestLegacy -import com.acon.core.data.dto.response.area.VerifiedAreaListResponse -import com.acon.core.data.dto.response.profile.PreSignedUrlResponse -import com.acon.core.data.dto.response.profile.ProfileResponseLegacy -import com.acon.core.data.dto.response.profile.SavedSpotsResponseLegacy -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.DELETE -import retrofit2.http.GET -import retrofit2.http.PATCH -import retrofit2.http.POST -import retrofit2.http.Path -import retrofit2.http.Query - -interface ProfileAuthApiLegacy { - @GET("/api/v1/members/me") - suspend fun fetchProfile(): ProfileResponseLegacy - - @GET("/api/v1/images/presigned-url?imageType=PROFILE") - suspend fun getPreSignedUrl() : PreSignedUrlResponse - - @GET("/api/v1/nickname/validate") - suspend fun validateNickname( - @Query("nickname", encoded = true) nickname: String - ): Response - - @PATCH("/api/v1/members/me") - suspend fun updateProfile( - @Body request: UpdateProfileRequestLegacy - ): Response - - @GET("/api/v1/saved-spots") - suspend fun fetchSavedSpots(): SavedSpotsResponseLegacy - - @POST("/api/v1/saved-spots") - suspend fun saveSpot( - @Body saveSpotRequest: SaveSpotRequest - ) - - @GET("/api/v1/verified-areas") - suspend fun fetchVerifiedAreaList() : VerifiedAreaListResponse - - @POST("/api/v1/verified-areas/replacement") - suspend fun replaceVerifiedArea( - @Body request: ReplaceVerifiedAreaRequest - ) - - @DELETE("/api/v1/verified-areas/{verifiedAreaId}") - suspend fun deleteVerifiedArea( - @Path("verifiedAreaId") verifiedAreaId: Long - ) -} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/SpotAuthApi.kt b/core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/SpotAuthApi.kt index 55d0beb93..5354323c5 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/SpotAuthApi.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/SpotAuthApi.kt @@ -4,7 +4,6 @@ import com.acon.core.data.dto.request.AddBookmarkRequest import com.acon.core.data.dto.request.SpotListRequest import com.acon.core.data.dto.response.SpotDetailResponse import com.acon.core.data.dto.response.SpotListResponse -import com.acon.core.data.dto.response.profile.SavedSpotsResponseLegacy import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET @@ -19,9 +18,6 @@ interface SpotAuthApi { @Body request: SpotListRequest ): SpotListResponse - @GET("/api/v1/saved-spots") - suspend fun fetchSavedSpotList(): SavedSpotsResponseLegacy - @POST("/api/v1/saved-spots") suspend fun addBookmark( @Body addBookmarkRequest: AddBookmarkRequest diff --git a/core/data/src/main/kotlin/com/acon/core/data/cache/ProfileInfoCacheLegacy.kt b/core/data/src/main/kotlin/com/acon/core/data/cache/ProfileInfoCacheLegacy.kt deleted file mode 100644 index 1149ccbe3..000000000 --- a/core/data/src/main/kotlin/com/acon/core/data/cache/ProfileInfoCacheLegacy.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.acon.core.data.cache - -import com.acon.acon.core.model.model.profile.ProfileInfoLegacy -import com.acon.core.data.cache.base.ReadWriteCache -import com.acon.core.data.datasource.remote.ProfileRemoteDataSourceLegacy -import kotlinx.coroutines.CoroutineScope -import javax.inject.Inject - -class ProfileInfoCacheLegacy @Inject constructor( - private val scope: CoroutineScope, - private val profileRemoteDataSourceLegacy: ProfileRemoteDataSourceLegacy -) : ReadWriteCache(scope) { - - override val emptyData = Result.success(ProfileInfoLegacy.Empty) - - override suspend fun fetchRemoteData(): ProfileInfoLegacy { - return profileRemoteDataSourceLegacy.fetchProfile().toProfile() - } -} diff --git a/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt b/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt index d28156442..d121a7eb7 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSource.kt @@ -1,7 +1,9 @@ package com.acon.core.data.datasource.remote import com.acon.core.data.api.remote.ProfileApi +import com.acon.core.data.dto.request.ReplaceVerifiedAreaRequest import com.acon.core.data.dto.request.profile.UpdateProfileRequest +import com.acon.core.data.dto.response.area.VerifiedAreaListResponse import com.acon.core.data.dto.response.profile.ProfileResponse import com.acon.core.data.dto.response.profile.SavedSpotResponse import javax.inject.Inject @@ -12,6 +14,8 @@ interface ProfileRemoteDataSource { suspend fun updateProfile(updateProfileRequest: UpdateProfileRequest) suspend fun validateNickname(nickname: String) suspend fun getSavedSpots() : List + suspend fun getVerifiedAreas(): VerifiedAreaListResponse + suspend fun deleteVerifiedArea(verifiedAreaId: Long) } class ProfileRemoteDataSourceImpl @Inject constructor( @@ -33,4 +37,12 @@ class ProfileRemoteDataSourceImpl @Inject constructor( override suspend fun getSavedSpots(): List { return profileApi.getSavedSpots().savedSpotList } + + override suspend fun getVerifiedAreas(): VerifiedAreaListResponse { + return profileApi.getVerifiedAreas() + } + + override suspend fun deleteVerifiedArea(verifiedAreaId: Long) { + return profileApi.deleteVerifiedArea(verifiedAreaId) + } } diff --git a/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceLegacy.kt b/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceLegacy.kt deleted file mode 100644 index bc0447d38..000000000 --- a/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/ProfileRemoteDataSourceLegacy.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.acon.core.data.datasource.remote - -import com.acon.core.data.dto.request.UpdateProfileRequestLegacy -import com.acon.core.data.dto.response.profile.PreSignedUrlResponse -import com.acon.core.data.dto.response.profile.ProfileResponseLegacy -import com.acon.core.data.api.remote.auth.ProfileAuthApiLegacy -import com.acon.core.data.dto.request.ReplaceVerifiedAreaRequest -import com.acon.core.data.dto.request.SaveSpotRequest -import com.acon.core.data.dto.response.area.VerifiedAreaListResponse -import retrofit2.Response -import javax.inject.Inject - -class ProfileRemoteDataSourceLegacy @Inject constructor( - private val profileAuthApiLegacy: ProfileAuthApiLegacy -) { - suspend fun fetchProfile(): ProfileResponseLegacy { - return profileAuthApiLegacy.fetchProfile() - } - - suspend fun getPreSignedUrl(): PreSignedUrlResponse { - return profileAuthApiLegacy.getPreSignedUrl() - } - - suspend fun validateNickname(nickname: String): Response { - return profileAuthApiLegacy.validateNickname(nickname) - } - - suspend fun updateProfile(fileName: String, nickname: String, birthday: String?): Response { - return profileAuthApiLegacy.updateProfile( - request = UpdateProfileRequestLegacy(profileImage = fileName, nickname = nickname, birthDate = formatBirthday(birthday)) - ) - } - - suspend fun fetchSavedSpots() = profileAuthApiLegacy.fetchSavedSpots() - suspend fun saveSpot(saveSpotRequest: SaveSpotRequest) = profileAuthApiLegacy.saveSpot(saveSpotRequest) - - suspend fun fetchVerifiedAreaList() : VerifiedAreaListResponse { - return profileAuthApiLegacy.fetchVerifiedAreaList() - } - - suspend fun replaceVerifiedArea( - previousVerifiedAreaId: Long, - latitude: Double, - longitude: Double - ) { - return profileAuthApiLegacy.replaceVerifiedArea( - request = ReplaceVerifiedAreaRequest( - previousVerifiedAreaId = previousVerifiedAreaId, - latitude = latitude, - longitude = longitude - ) - ) - } - - suspend fun deleteVerifiedArea(verifiedAreaId: Long) { - return profileAuthApiLegacy.deleteVerifiedArea(verifiedAreaId) - } -} - -private fun formatBirthday(birthday: String?): String? { - return birthday?.let { - "${it.substring(0, 4)}.${it.substring(4, 6)}.${it.substring(6, 8)}" - } -} diff --git a/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt b/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt index 83d685d7b..bddc46568 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/SpotRemoteDataSource.kt @@ -9,7 +9,6 @@ import com.acon.core.data.dto.request.SpotListRequest import com.acon.core.data.dto.response.MenuBoardListResponse import com.acon.core.data.dto.response.SpotDetailResponse import com.acon.core.data.dto.response.SpotListResponse -import com.acon.core.data.dto.response.profile.SavedSpotsResponseLegacy import javax.inject.Inject class SpotRemoteDataSource @Inject constructor( @@ -39,10 +38,6 @@ class SpotRemoteDataSource @Inject constructor( return spotAuthApi.fetchSpotDetailFromUser(spotId) } - suspend fun fetchSavedSpotList(): SavedSpotsResponseLegacy { - return spotAuthApi.fetchSavedSpotList() - } - suspend fun addBookmark(request: AddBookmarkRequest) { return spotAuthApi.addBookmark(request) } diff --git a/core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt b/core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt index c5f552ed1..9770727dd 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt @@ -9,7 +9,6 @@ import com.acon.core.data.api.remote.MapApi import com.acon.core.data.api.remote.MapSearchApi import com.acon.core.data.api.remote.ProfileApi import com.acon.core.data.api.remote.auth.OnboardingAuthApi -import com.acon.core.data.api.remote.auth.ProfileAuthApiLegacy import com.acon.core.data.api.remote.auth.SpotAuthApi import com.acon.core.data.api.remote.noauth.SpotNoAuthApi import com.acon.core.data.api.remote.auth.UploadAuthApi @@ -75,14 +74,6 @@ internal object ApiModule { return retrofit.create(UploadAuthApi::class.java) } - @Singleton - @Provides - fun providesProfileApiLegacy( - @Auth retrofit: Retrofit - ): ProfileAuthApiLegacy { - return retrofit.create(ProfileAuthApiLegacy::class.java) - } - @Singleton @Provides fun providesProfileApi( diff --git a/core/data/src/main/kotlin/com/acon/core/data/di/CacheModule.kt b/core/data/src/main/kotlin/com/acon/core/data/di/CacheModule.kt deleted file mode 100644 index cf977299b..000000000 --- a/core/data/src/main/kotlin/com/acon/core/data/di/CacheModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.acon.core.data.di - -import com.acon.acon.core.common.IODispatcher -import com.acon.core.data.cache.ProfileInfoCacheLegacy -import com.acon.core.data.datasource.remote.ProfileRemoteDataSourceLegacy -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object CacheModule { - - @Singleton - @Provides - fun providesProfileInfoCache( - @IODispatcher scope: CoroutineScope, - profileRemoteDataSourceLegacy: ProfileRemoteDataSourceLegacy - ) = ProfileInfoCacheLegacy(scope, profileRemoteDataSourceLegacy) -} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/di/DataStreamModule.kt b/core/data/src/main/kotlin/com/acon/core/data/di/DataStreamModule.kt new file mode 100644 index 000000000..9c6bfd4e1 --- /dev/null +++ b/core/data/src/main/kotlin/com/acon/core/data/di/DataStreamModule.kt @@ -0,0 +1,27 @@ +package com.acon.core.data.di + +import com.acon.core.data.stream.DataStream +import com.acon.core.data.stream.DataStreamImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Qualifier +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class DataStreamModule { + + @Singleton + @Binds + @VerifiedArea + abstract fun bindsVerifiedAreaDataStream( + impl: DataStreamImpl + ) : DataStream + +} + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class VerifiedArea \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt index f75d7a2b2..d46eea422 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/di/RepositoryModule.kt @@ -1,27 +1,25 @@ package com.acon.core.data.di -import com.acon.core.data.session.SessionHandler -import com.acon.core.data.session.SessionHandlerImpl -import com.acon.core.data.repository.AconAppRepositoryImpl -import com.acon.core.data.repository.MapRepositoryImpl -import com.acon.core.data.repository.MapSearchRepositoryImpl -import com.acon.core.data.repository.OnboardingRepositoryImpl -import com.acon.core.data.repository.ProfileRepositoryLegacyImpl -import com.acon.core.data.repository.SpotRepositoryImpl -import com.acon.core.data.repository.TimeRepositoryImpl -import com.acon.core.data.repository.UploadRepositoryImpl -import com.acon.core.data.repository.UserRepositoryImpl import com.acon.acon.domain.repository.AconAppRepository import com.acon.acon.domain.repository.MapRepository import com.acon.acon.domain.repository.MapSearchRepository import com.acon.acon.domain.repository.OnboardingRepository import com.acon.acon.domain.repository.ProfileRepository -import com.acon.acon.domain.repository.ProfileRepositoryLegacy import com.acon.acon.domain.repository.SpotRepository import com.acon.acon.domain.repository.TimeRepository import com.acon.acon.domain.repository.UploadRepository import com.acon.acon.domain.repository.UserRepository +import com.acon.core.data.repository.AconAppRepositoryImpl +import com.acon.core.data.repository.MapRepositoryImpl +import com.acon.core.data.repository.MapSearchRepositoryImpl +import com.acon.core.data.repository.OnboardingRepositoryImpl import com.acon.core.data.repository.ProfileRepositoryImpl +import com.acon.core.data.repository.SpotRepositoryImpl +import com.acon.core.data.repository.TimeRepositoryImpl +import com.acon.core.data.repository.UploadRepositoryImpl +import com.acon.core.data.repository.UserRepositoryImpl +import com.acon.core.data.session.SessionHandler +import com.acon.core.data.session.SessionHandlerImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -68,12 +66,6 @@ internal abstract class RepositoryModule { impl: UploadRepositoryImpl ): UploadRepository - @Singleton - @Binds - abstract fun bindsProfileRepositoryLegacy( - impl: ProfileRepositoryLegacyImpl - ): ProfileRepositoryLegacy - @Singleton @Binds abstract fun bindsMapRepository( diff --git a/core/data/src/main/kotlin/com/acon/core/data/dto/request/UpdateProfileRequestLegacy.kt b/core/data/src/main/kotlin/com/acon/core/data/dto/request/UpdateProfileRequestLegacy.kt deleted file mode 100644 index ef946ad9d..000000000 --- a/core/data/src/main/kotlin/com/acon/core/data/dto/request/UpdateProfileRequestLegacy.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.acon.core.data.dto.request - -import kotlinx.serialization.EncodeDefault -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class UpdateProfileRequestLegacy @OptIn(ExperimentalSerializationApi::class) constructor( - @SerialName("profileImage") val profileImage: String? = null, - @SerialName("nickname") val nickname: String, - @SerialName("birthDate") @EncodeDefault(EncodeDefault.Mode.NEVER) val birthDate: String? = null -) \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/dto/request/profile/UpdateProfileRequest.kt b/core/data/src/main/kotlin/com/acon/core/data/dto/request/profile/UpdateProfileRequest.kt index f78b8d66e..b05fcf524 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/dto/request/profile/UpdateProfileRequest.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/dto/request/profile/UpdateProfileRequest.kt @@ -17,7 +17,7 @@ fun Profile.toUpdateProfileRequest() : UpdateProfileRequest { val requestNickname = nickname val requestBirthDate: String? = when(birthDate) { is BirthDateStatus.Specified -> with((birthDate as BirthDateStatus.Specified).date) { - "$year.${monthValue.toString().padStart(2, '0')}.$dayOfMonth" + "%04d.%02d.%02d".format(year, monthValue, dayOfMonth) } BirthDateStatus.NotSpecified -> null } diff --git a/core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponseLegacy.kt b/core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponseLegacy.kt deleted file mode 100644 index 5667e149b..000000000 --- a/core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/ProfileResponseLegacy.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.acon.core.data.dto.response.profile - -import com.acon.acon.core.model.model.profile.ProfileInfoLegacy -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ProfileResponseLegacy( - @SerialName("profileImage") val image: String, - @SerialName("nickname") val nickname: String, - @SerialName("birthDate") val birthDate: String? = null, - @SerialName("savedSpotList") val savedSpotList: List, -) { - fun toProfile() = ProfileInfoLegacy( - image = image, - nickname = nickname, - birthDate = birthDate, - savedSpotLegacies = savedSpotList.map { it.toSavedSpot() } - ) -} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponseLegacy.kt b/core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponseLegacy.kt deleted file mode 100644 index b41b3d571..000000000 --- a/core/data/src/main/kotlin/com/acon/core/data/dto/response/profile/SavedSpotsResponseLegacy.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.acon.core.data.dto.response.profile - -import com.acon.acon.core.model.model.profile.SavedSpotLegacy -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class SavedSpotsResponseLegacy( - @SerialName("savedSpotList") val savedSpotResponseLegacyList: List? -) - -@Serializable -data class SavedSpotResponseLegacy( - @SerialName("spotId") val spotId: Long?, - @SerialName("name") val name: String?, - @SerialName("image") val image: String? -) { - - fun toSavedSpot() = SavedSpotLegacy( - spotId = spotId ?: 0L, - name = name.orEmpty(), - image = image.orEmpty() - ) -} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/repository/AconAppRepositoryImpl.kt b/core/data/src/main/kotlin/com/acon/core/data/repository/AconAppRepositoryImpl.kt index 498be3829..0fb4ce209 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/repository/AconAppRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/repository/AconAppRepositoryImpl.kt @@ -1,18 +1,57 @@ package com.acon.core.data.repository +import android.content.Context +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import com.acon.acon.core.model.type.ImageType import com.acon.core.data.datasource.remote.AconAppRemoteDataSource import com.acon.core.data.error.runCatchingWith import com.acon.acon.domain.error.app.FetchShouldUpdateError import com.acon.acon.domain.repository.AconAppRepository +import com.acon.core.data.dto.request.GetPresignedUrlRequest +import dagger.hilt.android.qualifiers.ApplicationContext +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody import javax.inject.Inject +import kotlin.collections.contains class AconAppRepositoryImpl @Inject constructor( + @ApplicationContext private val context: Context, private val aconAppRemoteDataSource: AconAppRemoteDataSource ) : AconAppRepository { + private val availableImageMimeTypes by lazy { + setOf("image/jpg", "image/jpeg", "image/png", "image/webp", "image/heic") + } + override suspend fun shouldUpdateApp(currentVersion: String): Result { return runCatchingWith(FetchShouldUpdateError()) { aconAppRemoteDataSource.fetchShouldUpdateApp(currentVersion).shouldUpdate ?: false } } + + override suspend fun uploadImage(imageType: ImageType, url: String): Result { + return runCatchingWith { + val contentUri = url.toUri() + val fileName = DocumentFile.fromSingleUri(context, contentUri)?.name + ?: error("Failed to read file name: $url") + + val presignedUrlResponse = aconAppRemoteDataSource.getPresignedUrl(GetPresignedUrlRequest( + imageType = imageType, + fileName = fileName + )) + + val uriMimeType = context.contentResolver.getType(contentUri) + val finalMimeType = if (availableImageMimeTypes.contains(uriMimeType)) uriMimeType!! else "image/jpeg" + + val inputStream = context.contentResolver.openInputStream(contentUri) + val requestBody = inputStream?.use { input -> + input.readBytes().toRequestBody(finalMimeType.toMediaTypeOrNull()) + } ?: error("Failed to read image content: $url") + + aconAppRemoteDataSource.uploadFile(presignedUrlResponse.presignedUrl, requestBody) + + return@runCatchingWith presignedUrlResponse.fileUrl + } + } } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/repository/OnboardingRepositoryImpl.kt b/core/data/src/main/kotlin/com/acon/core/data/repository/OnboardingRepositoryImpl.kt index f0c2fd68a..c31151cb4 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/repository/OnboardingRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/repository/OnboardingRepositoryImpl.kt @@ -5,8 +5,10 @@ import com.acon.acon.core.model.type.FoodType import com.acon.acon.domain.error.onboarding.PostTastePreferenceResultError import com.acon.acon.domain.error.onboarding.VerifyAreaError import com.acon.acon.domain.repository.OnboardingRepository +import com.acon.core.data.stream.DataStream import com.acon.core.data.datasource.local.OnboardingLocalDataSource import com.acon.core.data.datasource.remote.OnboardingRemoteDataSource +import com.acon.core.data.di.VerifiedArea import com.acon.core.data.dto.request.TastePreferenceRequest import com.acon.core.data.error.runCatchingWith import javax.inject.Inject @@ -14,6 +16,7 @@ import javax.inject.Inject class OnboardingRepositoryImpl @Inject constructor( private val onboardingRemoteDataSource: OnboardingRemoteDataSource, private val onboardingLocalDataSource: OnboardingLocalDataSource, + @VerifiedArea private val areaDataStream: DataStream ) : OnboardingRepository { override suspend fun submitTastePreferenceResult( @@ -35,6 +38,7 @@ class OnboardingRepositoryImpl @Inject constructor( longitude = longitude ) onboardingLocalDataSource.updateHasVerifiedArea(true) + areaDataStream.notifyDataChanged() } override suspend fun updateHasTastePreference(hasPreference: Boolean): Result { diff --git a/core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt b/core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt index 24dd0c143..547e44b3a 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryImpl.kt @@ -1,34 +1,34 @@ package com.acon.core.data.repository import android.content.Context -import androidx.core.net.toUri +import com.acon.acon.core.model.model.area.Area import com.acon.acon.core.model.model.profile.Profile import com.acon.acon.core.model.model.profile.ProfileImageStatus import com.acon.acon.core.model.model.profile.SavedSpot import com.acon.acon.core.model.type.ImageType +import com.acon.acon.domain.error.area.DeleteVerifiedAreaError import com.acon.acon.domain.error.profile.UpdateProfileError import com.acon.acon.domain.error.profile.ValidateNicknameError +import com.acon.acon.domain.repository.AconAppRepository import com.acon.acon.domain.repository.ProfileRepository -import com.acon.core.data.api.remote.noauth.FileUploadApi import com.acon.core.data.datasource.local.ProfileLocalDataSource -import com.acon.core.data.datasource.remote.AconAppRemoteDataSource import com.acon.core.data.datasource.remote.ProfileRemoteDataSource -import com.acon.core.data.dto.request.GetPresignedUrlRequest +import com.acon.core.data.di.VerifiedArea import com.acon.core.data.dto.request.profile.toUpdateProfileRequest import com.acon.core.data.error.runCatchingWith +import com.acon.core.data.stream.DataStream import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.RequestBody.Companion.toRequestBody import javax.inject.Inject class ProfileRepositoryImpl @Inject constructor( private val profileRemoteDataSource: ProfileRemoteDataSource, private val profileLocalDataSource: ProfileLocalDataSource, - private val aconAppRemoteDataSource: AconAppRemoteDataSource, + private val aconAppRepository: AconAppRepository, + @VerifiedArea private val areaDataStream: DataStream, @ApplicationContext private val context: Context ) : ProfileRepository { @@ -62,18 +62,11 @@ class ProfileRepositoryImpl @Inject constructor( val imageStatus = newProfile.image if (imageStatus is ProfileImageStatus.Custom) { if (imageStatus.url.startsWith("content://")) { - val presignedUrlResponse = aconAppRemoteDataSource.getPresignedUrl(GetPresignedUrlRequest( - imageType = ImageType.PROFILE, - fileName = imageStatus.url - )) - val inputStream = context.contentResolver.openInputStream(imageStatus.url.toUri()) - val requestBody = inputStream?.readBytes()?.toRequestBody("image/jpeg".toMediaTypeOrNull()) - requestBody?.let { - aconAppRemoteDataSource.uploadFile(presignedUrlResponse.presignedUrl, it) - } + val uploadUrlResult = aconAppRepository.uploadImage(ImageType.PROFILE, imageStatus.url) + val fileUrl = uploadUrlResult.getOrThrow() profileToUpdate = newProfile.copy( - image = ProfileImageStatus.Custom(presignedUrlResponse.fileUrl) + image = ProfileImageStatus.Custom(fileUrl) ) } else { profileToUpdate = newProfile @@ -118,4 +111,20 @@ class ProfileRepositoryImpl @Inject constructor( }) } } -} \ No newline at end of file + + override fun getVerifiedAreas(): Flow>> { + return areaDataStream.subscribe { + emit(runCatchingWith() { + profileRemoteDataSource.getVerifiedAreas().verifiedAreaList + .map { it.toVerifiedArea() } + }) + } + } + + override suspend fun deleteVerifiedArea(verifiedAreaId: Long): Result { + return runCatchingWith(DeleteVerifiedAreaError()) { + profileRemoteDataSource.deleteVerifiedArea(verifiedAreaId) + areaDataStream.notifyDataChanged() + } + } +} diff --git a/core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryLegacyImpl.kt b/core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryLegacyImpl.kt deleted file mode 100644 index a6617da32..000000000 --- a/core/data/src/main/kotlin/com/acon/core/data/repository/ProfileRepositoryLegacyImpl.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.acon.core.data.repository - -import com.acon.acon.core.common.IODispatcher -import com.acon.acon.core.model.model.area.Area -import com.acon.core.data.cache.ProfileInfoCacheLegacy -import com.acon.core.data.datasource.remote.ProfileRemoteDataSourceLegacy -import com.acon.core.data.dto.request.SaveSpotRequest -import com.acon.core.data.error.runCatchingWith -import com.acon.acon.domain.error.profile.SaveSpotError -import com.acon.acon.domain.error.profile.ValidateNicknameErrorLegacy -import com.acon.acon.core.model.model.profile.PreSignedUrl -import com.acon.acon.core.model.model.profile.ProfileInfoLegacy -import com.acon.acon.core.model.model.profile.SavedSpotLegacy -import com.acon.acon.domain.repository.ProfileRepositoryLegacy -import com.acon.acon.core.model.type.UpdateProfileType -import com.acon.acon.domain.error.area.DeleteVerifiedAreaError -import com.acon.acon.domain.error.area.ReplaceVerifiedArea -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -class ProfileRepositoryLegacyImpl @Inject constructor( - @IODispatcher private val scope: CoroutineScope, - private val profileRemoteDataSourceLegacy: ProfileRemoteDataSourceLegacy, - private val profileInfoCacheLegacy: ProfileInfoCacheLegacy -) : ProfileRepositoryLegacy { - - override fun fetchProfile(): Flow> { - return flow { - emit(runCatchingWith { - profileRemoteDataSourceLegacy.fetchProfile().toProfile() - }) - } - return profileInfoCacheLegacy.data - } - - override suspend fun getPreSignedUrl(): Result { - return runCatchingWith() { - profileRemoteDataSourceLegacy.getPreSignedUrl().toPreSignedUrl() - } - } - - override suspend fun validateNickname(nickname: String): Result { - return runCatchingWith(ValidateNicknameErrorLegacy()) { - profileRemoteDataSourceLegacy.validateNickname(nickname) - } - } - - override suspend fun updateProfile(fileName: String, nickname: String, birthday: String?, uri: String): Result { - return runCatchingWith() { - profileRemoteDataSourceLegacy.updateProfile(fileName, nickname, birthday) - profileInfoCacheLegacy.updateData( - ProfileInfoLegacy( - nickname = nickname, - birthDate = birthday, - image = uri, - savedSpotLegacies = profileInfoCacheLegacy.data.value.getOrNull()?.savedSpotLegacies.orEmpty() - ) - ) - } - } - - private val _updateProfileType = MutableStateFlow(UpdateProfileType.IDLE) - private val updateProfileType = flow { - emitAll(_updateProfileType) - }.stateIn( - scope = scope, - started = SharingStarted.Lazily, - initialValue = UpdateProfileType.IDLE - ) - - override fun updateProfileType(type: UpdateProfileType) { - _updateProfileType.value = type - } - - override fun getProfileType(): Flow { - return updateProfileType - } - - override suspend fun resetProfileType() { - _updateProfileType.emit(UpdateProfileType.IDLE) - } - - override suspend fun fetchSavedSpots(): Result> { - return runCatchingWith() { - profileRemoteDataSourceLegacy.fetchSavedSpots().savedSpotResponseLegacyList?.map { - it.toSavedSpot() - }.orEmpty() - } - } - - override suspend fun saveSpot(spotId: Long): Result { - return runCatchingWith(SaveSpotError()) { - profileRemoteDataSourceLegacy.saveSpot(SaveSpotRequest(spotId)) - } - } - - override suspend fun fetchVerifiedAreaList(): Result> { - // TODO - 인증 지역 조회 API Error 처리 안됨 - return runCatchingWith() { - profileRemoteDataSourceLegacy.fetchVerifiedAreaList().verifiedAreaList - .map { it.toVerifiedArea() } - } - } - - override suspend fun replaceVerifiedArea( - previousVerifiedAreaId: Long, - latitude: Double, - longitude: Double - ): Result { - return runCatchingWith(ReplaceVerifiedArea()) { - profileRemoteDataSourceLegacy.replaceVerifiedArea( - previousVerifiedAreaId = previousVerifiedAreaId, - latitude = latitude, - longitude = longitude - ) - } - } - - override suspend fun deleteVerifiedArea(verifiedAreaId: Long): Result { - return runCatchingWith(DeleteVerifiedAreaError()) { - profileRemoteDataSourceLegacy.deleteVerifiedArea(verifiedAreaId) - } - } -} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt b/core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt index 6e929ae20..92c78b283 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/repository/SpotRepositoryImpl.kt @@ -1,37 +1,32 @@ package com.acon.core.data.repository -import com.acon.acon.core.model.model.profile.SavedSpotLegacy import com.acon.acon.core.model.model.spot.Condition import com.acon.acon.core.model.model.spot.MenuBoardList import com.acon.acon.core.model.model.spot.SpotDetail import com.acon.acon.core.model.model.spot.SpotList -import com.acon.core.data.cache.ProfileInfoCacheLegacy -import com.acon.core.data.datasource.remote.SpotRemoteDataSource -import com.acon.core.data.dto.request.AddBookmarkRequest -import com.acon.core.data.dto.request.ConditionRequest -import com.acon.core.data.dto.request.FilterListRequest -import com.acon.core.data.dto.request.RecentNavigationLocationRequest -import com.acon.core.data.dto.request.SpotListRequest -import com.acon.core.data.error.runCatchingWith -import com.acon.core.data.session.SessionHandler import com.acon.acon.domain.error.spot.AddBookmarkError import com.acon.acon.domain.error.spot.DeleteBookmarkError import com.acon.acon.domain.error.spot.FetchMenuBoardsError import com.acon.acon.domain.error.spot.FetchRecentNavigationLocationError import com.acon.acon.domain.error.spot.FetchSpotListError import com.acon.acon.domain.error.spot.GetSpotDetailInfoError -import com.acon.acon.domain.repository.ProfileRepositoryLegacy import com.acon.acon.domain.repository.SpotRepository import com.acon.core.data.datasource.local.ProfileLocalDataSource import com.acon.core.data.datasource.remote.ProfileRemoteDataSource +import com.acon.core.data.datasource.remote.SpotRemoteDataSource +import com.acon.core.data.dto.request.AddBookmarkRequest +import com.acon.core.data.dto.request.ConditionRequest +import com.acon.core.data.dto.request.FilterListRequest +import com.acon.core.data.dto.request.RecentNavigationLocationRequest +import com.acon.core.data.dto.request.SpotListRequest +import com.acon.core.data.error.runCatchingWith +import com.acon.core.data.session.SessionHandler import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import javax.inject.Inject class SpotRepositoryImpl @Inject constructor( private val spotRemoteDataSource: SpotRemoteDataSource, - private val profileInfoCacheLegacy: ProfileInfoCacheLegacy, - private val profileRepositoryLegacy: ProfileRepositoryLegacy, private val profileLocalDataSource: ProfileLocalDataSource, private val profileRemoteDataSource: ProfileRemoteDataSource, private val sessionHandler: SessionHandler @@ -94,14 +89,6 @@ class SpotRepositoryImpl @Inject constructor( } } - override suspend fun fetchSavedSpotList(): Result> { - return runCatchingWith() { - spotRemoteDataSource.fetchSavedSpotList().savedSpotResponseLegacyList?.map { - it.toSavedSpot() - }.orEmpty() - } - } - override suspend fun addBookmark(spotId: Long): Result { return runCatchingWith(AddBookmarkError()) { spotRemoteDataSource.addBookmark(AddBookmarkRequest(spotId)) @@ -111,13 +98,6 @@ class SpotRepositoryImpl @Inject constructor( profileLocalDataSource.cacheSavedSpots(profileRemoteDataSource.getSavedSpots().map { it.toSavedSpot() }) - - profileRepositoryLegacy.fetchSavedSpots().onSuccess { fetched -> - (profileInfoCacheLegacy.data.value.getOrNull() - ?: return@onSuccess).let { profileInfo -> - profileInfoCacheLegacy.updateData(profileInfo.copy(savedSpotLegacies = fetched)) - } - } } } @@ -125,12 +105,11 @@ class SpotRepositoryImpl @Inject constructor( return runCatchingWith(DeleteBookmarkError()) { spotRemoteDataSource.deleteBookmark(spotId) - profileRepositoryLegacy.fetchSavedSpots().onSuccess { fetched -> - (profileInfoCacheLegacy.data.value.getOrNull() - ?: return@onSuccess).let { profileInfo -> - profileInfoCacheLegacy.updateData(profileInfo.copy(savedSpotLegacies = fetched)) - } - } + val cachedSavedSpots = profileLocalDataSource.getSavedSpots().firstOrNull() + if (cachedSavedSpots != null) + profileLocalDataSource.cacheSavedSpots(profileRemoteDataSource.getSavedSpots().map { + it.toSavedSpot() + }) } } } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt b/core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt index 0cd5342d9..050e06fa9 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/repository/UserRepositoryImpl.kt @@ -7,9 +7,7 @@ import com.acon.acon.data.dto.request.DeleteAccountRequest import com.acon.acon.domain.error.user.PostSignInError import com.acon.acon.domain.error.user.PostSignOutError import com.acon.acon.domain.repository.OnboardingRepository -import com.acon.acon.domain.repository.ProfileRepository import com.acon.acon.domain.repository.UserRepository -import com.acon.core.data.cache.ProfileInfoCacheLegacy import com.acon.core.data.datasource.local.ProfileLocalDataSource import com.acon.core.data.datasource.local.TokenLocalDataSource import com.acon.core.data.datasource.remote.UserRemoteDataSource @@ -26,7 +24,6 @@ class UserRepositoryImpl @Inject constructor( private val userRemoteDataSource: UserRemoteDataSource, private val tokenLocalDataSource: TokenLocalDataSource, private val sessionHandler: SessionHandler, - private val profileInfoCacheLegacy: ProfileInfoCacheLegacy, private val onboardingRepository: OnboardingRepository, private val profileLocalDataSource: ProfileLocalDataSource ) : UserRepository { @@ -96,7 +93,6 @@ class UserRepositoryImpl @Inject constructor( } override suspend fun clearSession() = runCatchingWith { - profileInfoCacheLegacy.clearData() sessionHandler.clearSession() } } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/stream/DataStream.kt b/core/data/src/main/kotlin/com/acon/core/data/stream/DataStream.kt new file mode 100644 index 000000000..0f1d6e9ea --- /dev/null +++ b/core/data/src/main/kotlin/com/acon/core/data/stream/DataStream.kt @@ -0,0 +1,28 @@ +package com.acon.core.data.stream + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.transformLatest +import javax.inject.Inject + +interface DataStream { + suspend fun notifyDataChanged() + fun subscribe(block: suspend FlowCollector.() -> Unit): Flow +} + +class DataStreamImpl @Inject constructor() : DataStream { + private val trigger = MutableSharedFlow(replay = 1) + + init { + trigger.tryEmit(Unit) + } + + override suspend fun notifyDataChanged() { + trigger.emit(Unit) + } + + override fun subscribe(block: suspend FlowCollector.() -> Unit): Flow { + return trigger.transformLatest { this.block() } + } +} \ No newline at end of file diff --git a/core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryImplTest.kt b/core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryImplTest.kt deleted file mode 100644 index f1db63514..000000000 --- a/core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryImplTest.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.acon.core.data.repository - -import com.acon.acon.domain.error.area.DeleteVerifiedAreaError -import com.acon.acon.domain.error.area.ReplaceVerifiedArea -import com.acon.acon.domain.error.profile.SaveSpotError -import com.acon.acon.domain.error.profile.ValidateNicknameErrorLegacy -import com.acon.core.data.assertValidErrorMapping -import com.acon.core.data.cache.ProfileInfoCacheLegacy -import com.acon.core.data.createErrorStream -import com.acon.core.data.createFakeRemoteError -import com.acon.core.data.datasource.remote.ProfileRemoteDataSourceLegacy -import io.mockk.coEvery -import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.junit5.MockKExtension -import kotlinx.coroutines.cancel -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource -import kotlin.reflect.KClass - -@ExtendWith(MockKExtension::class) -class ProfileRepositoryImplTest { - - @RelaxedMockK - lateinit var profileRemoteDataSourceLegacy: ProfileRemoteDataSourceLegacy - - @RelaxedMockK - lateinit var profileInfoCacheLegacy: ProfileInfoCacheLegacy - - private lateinit var testScope: TestScope - - lateinit var profileRepositoryImpl: ProfileRepositoryLegacyImpl - - companion object { - @JvmStatic - fun validNicknameErrorScenarios() = createErrorStream( - 40051 to ValidateNicknameErrorLegacy.UnsatisfiedCondition::class, - 40901 to ValidateNicknameErrorLegacy.AlreadyUsedNickname::class - ) - @JvmStatic - fun saveSpotErrorScenarios() = createErrorStream( - 40403 to SaveSpotError.NotExistSpot::class - ) - @JvmStatic - fun replaceVerifiedAreaErrorScenarios() = createErrorStream( - 40012 to ReplaceVerifiedArea.OutOfServiceAreaError::class, - 40054 to ReplaceVerifiedArea.InvalidVerifiedArea::class, - 40055 to ReplaceVerifiedArea.PeriodRestrictedDeleteError::class, - 40056 to ReplaceVerifiedArea.MultiLocationReplaceError::class, - 40404 to ReplaceVerifiedArea.VerifiedAreaNotFound::class - ) - @JvmStatic - fun deleteVerifiedAreaErrorScenarios() = createErrorStream( - 40054 to DeleteVerifiedAreaError.InvalidVerifiedArea::class, - 40032 to DeleteVerifiedAreaError.VerifiedAreaLimitViolation::class, - 40055 to DeleteVerifiedAreaError.PeriodRestrictedDeleteError::class, - 40404 to DeleteVerifiedAreaError.VerifiedAreaNotFound::class - ) - } - - @BeforeEach - fun setUp() { - testScope = TestScope() - profileRepositoryImpl = ProfileRepositoryLegacyImpl(testScope, profileRemoteDataSourceLegacy, profileInfoCacheLegacy) - } - - @AfterEach - fun tearDown() { - testScope.cancel() - } - - @ParameterizedTest - @MethodSource("validNicknameErrorScenarios") - fun `닉네임 유효성 검사 API 실패 시 에러 객체를 반환한다`( - errorCode: Int, - expectedErrorClass: KClass - ) = runTest { - // Given - val fakeRemoteError = createFakeRemoteError(errorCode) - coEvery { profileRemoteDataSourceLegacy.validateNickname(any()) } throws fakeRemoteError - - // When - val result = profileRepositoryImpl.validateNickname("") - - // Then - assertValidErrorMapping(result, expectedErrorClass) - } - - @ParameterizedTest - @MethodSource("saveSpotErrorScenarios") - fun `장소 저장 API 실패 시 에러 객체를 반환한다`( - errorCode: Int, - expectedErrorClass: KClass - ) = runTest { - // Given - val fakeRemoteError = createFakeRemoteError(errorCode) - coEvery { profileRemoteDataSourceLegacy.saveSpot(any()) } throws fakeRemoteError - - // When - val result = profileRepositoryImpl.saveSpot(0) - - // Then - assertValidErrorMapping(result, expectedErrorClass) - } - - @ParameterizedTest - @MethodSource("replaceVerifiedAreaErrorScenarios") - fun `인증 지역 변경 API 실패 시 에러 객체를 반환한다`( - errorCode: Int, - expectedErrorClass: KClass - ) = runTest { - // Given - val fakeRemoteError = createFakeRemoteError(errorCode) - coEvery { profileRemoteDataSourceLegacy.replaceVerifiedArea(any(), any(), any()) } throws fakeRemoteError - - // When - val result = profileRepositoryImpl.replaceVerifiedArea(0, .0, .0) - - // Then - assertValidErrorMapping(result, expectedErrorClass) - } - - @ParameterizedTest - @MethodSource("deleteVerifiedAreaErrorScenarios") - fun `인증 지역 삭제 API 실패 시 에러 객체를 반환한다`( - errorCode: Int, - expectedErrorClass: KClass - ) = runTest { - // Given - val fakeRemoteError = createFakeRemoteError(errorCode) - coEvery { profileRemoteDataSourceLegacy.deleteVerifiedArea(any()) } throws fakeRemoteError - - // When - val result = profileRepositoryImpl.deleteVerifiedArea(0) - - // Then - assertValidErrorMapping(result, expectedErrorClass) - } -} \ No newline at end of file diff --git a/core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryTest.kt b/core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryTest.kt index 89088c206..6ca9005c5 100644 --- a/core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryTest.kt +++ b/core/data/src/test/java/com/acon/core/data/repository/ProfileRepositoryTest.kt @@ -16,6 +16,7 @@ import com.acon.core.data.datasource.remote.AconAppRemoteDataSource import com.acon.core.data.datasource.remote.ProfileRemoteDataSource import com.acon.core.data.dto.response.profile.ProfileResponse import com.acon.core.data.dto.response.profile.SavedSpotResponse +import com.acon.core.data.stream.DataStream import io.mockk.coEvery import io.mockk.coVerify import io.mockk.impl.annotations.MockK @@ -48,6 +49,9 @@ class ProfileRepositoryTest { @MockK private lateinit var aconAppRemoteDataSource: AconAppRemoteDataSource + @MockK + private lateinit var dataStream: DataStream + private lateinit var profileRepository: ProfileRepository private val sampleNewProfile get() = Profile( @@ -59,7 +63,7 @@ class ProfileRepositoryTest { @BeforeEach fun setUp() { profileRepository = ProfileRepositoryImpl( - profileRemoteDataSource, profileLocalDataSource, aconAppRemoteDataSource, + profileRemoteDataSource, profileLocalDataSource, aconAppRemoteDataSource, dataStream, mockk(relaxed = true) ) } diff --git a/core/data/src/test/java/com/acon/core/data/repository/SpotRepositoryImplTest.kt b/core/data/src/test/java/com/acon/core/data/repository/SpotRepositoryImplTest.kt index 59c260e00..1d35d5329 100644 --- a/core/data/src/test/java/com/acon/core/data/repository/SpotRepositoryImplTest.kt +++ b/core/data/src/test/java/com/acon/core/data/repository/SpotRepositoryImplTest.kt @@ -6,9 +6,7 @@ import com.acon.acon.domain.error.spot.FetchMenuBoardsError import com.acon.acon.domain.error.spot.FetchRecentNavigationLocationError import com.acon.acon.domain.error.spot.FetchSpotListError import com.acon.acon.domain.error.spot.GetSpotDetailInfoError -import com.acon.acon.domain.repository.ProfileRepositoryLegacy import com.acon.core.data.assertValidErrorMapping -import com.acon.core.data.cache.ProfileInfoCacheLegacy import com.acon.core.data.createErrorStream import com.acon.core.data.createFakeRemoteError import com.acon.core.data.datasource.remote.SpotRemoteDataSource @@ -29,12 +27,6 @@ class SpotRepositoryImplTest { @RelaxedMockK lateinit var spotRemoteDataSource: SpotRemoteDataSource - @RelaxedMockK - lateinit var profileInfoCacheLegacy: ProfileInfoCacheLegacy - - @RelaxedMockK - lateinit var profileRepositoryLegacy: ProfileRepositoryLegacy - @InjectMockKs lateinit var spotRepositoryImpl: SpotRepositoryImpl diff --git a/core/data/src/test/java/com/acon/core/data/stream/DataStreamImplTest.kt b/core/data/src/test/java/com/acon/core/data/stream/DataStreamImplTest.kt new file mode 100644 index 000000000..100ca27dc --- /dev/null +++ b/core/data/src/test/java/com/acon/core/data/stream/DataStreamImplTest.kt @@ -0,0 +1,75 @@ +package com.acon.core.data.stream + +import app.cash.turbine.test +import io.mockk.junit5.MockKExtension +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +class DataStreamImplTest { + + private lateinit var dataStream: DataStream + + @BeforeEach + fun setUp() { + dataStream = DataStreamImpl() + } + + @Test + fun `초기 방출 테스트`() = runTest { + var value = 0 + val flow = dataStream.subscribe { + emit(++value) + } + + flow.test { + assertEquals(1, awaitItem()) + } + } + + @Test + fun `notifyDataChanged가 flow를 트리거하는지 테스트`() = runTest { + var value = 0 + val flow = dataStream.subscribe { + emit(++value) + } + + flow.test { + assertEquals(1, awaitItem()) + dataStream.notifyDataChanged() + assertEquals(2, awaitItem()) + dataStream.notifyDataChanged() + assertEquals(3, awaitItem()) + } + } + + @Test + fun `여러 구독이 생겼을 때, 한 번의 notify는 모든 구독자에게 전파되어야 한다`() = runTest { + var value1 = 0 + val flow1 = dataStream.subscribe { + emit(++value1) + } + + var value2 = 10 + val flow2 = dataStream.subscribe { + emit(++value2) + } + + flow1.test { + val f1 = this + flow2.test { + val f2 = this + assertEquals(1, f1.awaitItem()) // flow1의 첫 번째 값은 1 + assertEquals(11, f2.awaitItem()) // flow2의 첫 번째 값은 11 + + dataStream.notifyDataChanged() + + assertEquals(2, f1.awaitItem()) // flow1의 두 번째 값은 2 + assertEquals(12, f2.awaitItem()) // flow2의 두 번째 값은 12 + } + } + } +} \ No newline at end of file diff --git a/core/model/src/main/java/com/acon/acon/core/model/model/profile/PresignedUrl.kt b/core/model/src/main/java/com/acon/acon/core/model/model/profile/PresignedUrl.kt new file mode 100644 index 000000000..766cc55d0 --- /dev/null +++ b/core/model/src/main/java/com/acon/acon/core/model/model/profile/PresignedUrl.kt @@ -0,0 +1,6 @@ +package com.acon.acon.core.model.model.profile + +data class PreSignedUrl( + val fileName: String, + val preSignedUrl: String +) \ No newline at end of file diff --git a/core/model/src/main/java/com/acon/acon/core/model/model/profile/ProfileInfoLegacy.kt b/core/model/src/main/java/com/acon/acon/core/model/model/profile/ProfileInfoLegacy.kt deleted file mode 100644 index 755ad28ca..000000000 --- a/core/model/src/main/java/com/acon/acon/core/model/model/profile/ProfileInfoLegacy.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.acon.acon.core.model.model.profile - -data class ProfileInfoLegacy( - val image: String, - val nickname: String, - val birthDate: String?, - val savedSpotLegacies: List, -) { - companion object { - val Empty = com.acon.acon.core.model.model.profile.ProfileInfoLegacy("", "", null, emptyList()) - } -} - -data class PreSignedUrl( - val fileName: String, - val preSignedUrl: String -) \ No newline at end of file diff --git a/core/model/src/main/java/com/acon/acon/core/model/model/profile/SavedSpotLegacy.kt b/core/model/src/main/java/com/acon/acon/core/model/model/profile/SavedSpotLegacy.kt deleted file mode 100644 index 344a4a4da..000000000 --- a/core/model/src/main/java/com/acon/acon/core/model/model/profile/SavedSpotLegacy.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.acon.acon.core.model.model.profile - -data class SavedSpotLegacy( - val spotId: Long, - val name: String, - val image: String -) \ No newline at end of file diff --git a/domain/src/main/java/com/acon/acon/domain/error/profile/ValidateNicknameErrorLegacy.kt b/domain/src/main/java/com/acon/acon/domain/error/profile/ValidateNicknameErrorLegacy.kt deleted file mode 100644 index f4d43b40e..000000000 --- a/domain/src/main/java/com/acon/acon/domain/error/profile/ValidateNicknameErrorLegacy.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.acon.acon.domain.error.profile - -import com.acon.acon.domain.error.RootError - -open class ValidateNicknameErrorLegacy : RootError() { - - class UnsatisfiedCondition : ValidateNicknameErrorLegacy() { - override val code: Int = 40051 - } - class AlreadyUsedNickname : ValidateNicknameErrorLegacy() { - override val code: Int = 40901 - } - - final override fun createErrorInstances(): Array { - return arrayOf( - UnsatisfiedCondition(), - AlreadyUsedNickname() - ) - } -} \ No newline at end of file diff --git a/domain/src/main/java/com/acon/acon/domain/repository/AconAppRepository.kt b/domain/src/main/java/com/acon/acon/domain/repository/AconAppRepository.kt index 629bf7eec..0e71e6658 100644 --- a/domain/src/main/java/com/acon/acon/domain/repository/AconAppRepository.kt +++ b/domain/src/main/java/com/acon/acon/domain/repository/AconAppRepository.kt @@ -1,5 +1,8 @@ package com.acon.acon.domain.repository +import com.acon.acon.core.model.type.ImageType + interface AconAppRepository { suspend fun shouldUpdateApp(currentVersion: String): Result + suspend fun uploadImage(imageType: ImageType, url: String): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/acon/acon/domain/repository/ProfileRepository.kt b/domain/src/main/java/com/acon/acon/domain/repository/ProfileRepository.kt index 7b50684a1..ce49030fe 100644 --- a/domain/src/main/java/com/acon/acon/domain/repository/ProfileRepository.kt +++ b/domain/src/main/java/com/acon/acon/domain/repository/ProfileRepository.kt @@ -1,5 +1,6 @@ package com.acon.acon.domain.repository +import com.acon.acon.core.model.model.area.Area import com.acon.acon.core.model.model.profile.Profile import com.acon.acon.core.model.model.profile.SavedSpot import kotlinx.coroutines.flow.Flow @@ -10,4 +11,6 @@ interface ProfileRepository { suspend fun updateProfile(newProfile: Profile) : Result suspend fun validateNickname(nickname: String) : Result suspend fun getSavedSpots() : Flow>> + fun getVerifiedAreas(): Flow>> + suspend fun deleteVerifiedArea(verifiedAreaId: Long): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/acon/acon/domain/repository/ProfileRepositoryLegacy.kt b/domain/src/main/java/com/acon/acon/domain/repository/ProfileRepositoryLegacy.kt deleted file mode 100644 index 66bec6fff..000000000 --- a/domain/src/main/java/com/acon/acon/domain/repository/ProfileRepositoryLegacy.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.acon.acon.domain.repository - -import com.acon.acon.core.model.model.area.Area -import com.acon.acon.core.model.model.profile.PreSignedUrl -import com.acon.acon.core.model.model.profile.ProfileInfoLegacy -import com.acon.acon.core.model.model.profile.SavedSpotLegacy -import com.acon.acon.core.model.type.UpdateProfileType -import kotlinx.coroutines.flow.Flow - -interface ProfileRepositoryLegacy { - fun fetchProfile(): Flow> - - suspend fun getPreSignedUrl(): Result - - suspend fun validateNickname(nickname: String): Result - - suspend fun updateProfile(fileName: String, nickname: String, birthday: String?, uri: String): Result - - fun getProfileType(): Flow - - fun updateProfileType(type: UpdateProfileType) - - suspend fun resetProfileType() - - suspend fun fetchSavedSpots(): Result> - - suspend fun saveSpot(spotId: Long): Result - - suspend fun fetchVerifiedAreaList(): Result> - - suspend fun replaceVerifiedArea( - previousVerifiedAreaId: Long, - latitude: Double, - longitude: Double - ): Result - - suspend fun deleteVerifiedArea(verifiedAreaId: Long): Result -} \ No newline at end of file diff --git a/domain/src/main/java/com/acon/acon/domain/repository/SpotRepository.kt b/domain/src/main/java/com/acon/acon/domain/repository/SpotRepository.kt index 0b9f8fd77..b7dbc1b95 100644 --- a/domain/src/main/java/com/acon/acon/domain/repository/SpotRepository.kt +++ b/domain/src/main/java/com/acon/acon/domain/repository/SpotRepository.kt @@ -1,6 +1,5 @@ package com.acon.acon.domain.repository -import com.acon.acon.core.model.model.profile.SavedSpotLegacy import com.acon.acon.core.model.model.spot.Condition import com.acon.acon.core.model.model.spot.MenuBoardList import com.acon.acon.core.model.model.spot.SpotDetail @@ -31,8 +30,6 @@ interface SpotRepository { spotId: Long ): Result - suspend fun fetchSavedSpotList(): Result> - suspend fun addBookmark( spotId: Long ): Result diff --git a/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreenContainer.kt b/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreenContainer.kt index 3eb066524..23237f7d5 100644 --- a/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreenContainer.kt +++ b/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreenContainer.kt @@ -22,6 +22,8 @@ import org.orbitmvi.orbit.compose.collectSideEffect fun AreaVerificationScreenContainer( onNavigateToVerifyInMap: () -> Unit, onNavigateToChooseDislikes: () -> Unit, + onNavigateToIntroduce: () -> Unit, + onNavigateToSpotList: () -> Unit, skippable: Boolean, modifier: Modifier = Modifier, viewModel: AreaVerificationViewModel = hiltViewModel(creationCallback = { factory: AreaVerificationViewModel.Factory -> @@ -73,11 +75,14 @@ fun AreaVerificationScreenContainer( } is AreaVerificationSideEffect.NavigateToChooseDislikes -> onNavigateToChooseDislikes() + is AreaVerificationSideEffect.NavigateToIntroduce -> onNavigateToIntroduce() + is AreaVerificationSideEffect.NavigateToSpotList -> onNavigateToSpotList() } } val navController = LocalNavController.current BackHandler { - navController.navigateUp() + if (!skippable) + navController.popBackStack() } } \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/viewmodel/AreaVerificationViewModel.kt b/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/viewmodel/AreaVerificationViewModel.kt index b1fa14512..5536c2f73 100644 --- a/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/viewmodel/AreaVerificationViewModel.kt +++ b/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/viewmodel/AreaVerificationViewModel.kt @@ -2,6 +2,7 @@ package com.acon.feature.onboarding.area.viewmodel import com.acon.acon.core.model.type.UserActionType import com.acon.acon.core.ui.base.BaseContainerHost +import com.acon.acon.domain.repository.OnboardingRepository import com.acon.acon.domain.repository.TimeRepository import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -12,6 +13,7 @@ import org.orbitmvi.orbit.viewmodel.container @HiltViewModel(assistedFactory = AreaVerificationViewModel.Factory::class) class AreaVerificationViewModel @AssistedInject constructor( private val timeRepository: TimeRepository, + private val onboardingRepository: OnboardingRepository, @Assisted private val shouldShowSkipButton: Boolean ) : BaseContainerHost() { @@ -28,8 +30,18 @@ class AreaVerificationViewModel @AssistedInject constructor( } fun onSkipClicked() = intent { - postSideEffect(AreaVerificationSideEffect.NavigateToChooseDislikes) timeRepository.saveUserActionTime(UserActionType.SKIP_AREA_VERIFICATION, System.currentTimeMillis()) + + onboardingRepository.getOnboardingPreferences().onSuccess { pref -> + if (pref.hasTastePreference.not()) + postSideEffect(AreaVerificationSideEffect.NavigateToChooseDislikes) + else if (pref.shouldShowIntroduce) + postSideEffect(AreaVerificationSideEffect.NavigateToIntroduce) + else + postSideEffect(AreaVerificationSideEffect.NavigateToSpotList) + }.onFailure { + postSideEffect(AreaVerificationSideEffect.NavigateToChooseDislikes) + } } @AssistedFactory @@ -57,4 +69,6 @@ sealed interface AreaVerificationSideEffect { data class ShowErrorToast(val errorMessage: String) : AreaVerificationSideEffect data object NavigateToChooseDislikes : AreaVerificationSideEffect + data object NavigateToIntroduce : AreaVerificationSideEffect + data object NavigateToSpotList : AreaVerificationSideEffect } \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/GallerySelectBottomSheet.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/GallerySelectBottomSheet.kt deleted file mode 100644 index 14f398596..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/GallerySelectBottomSheet.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.acon.acon.feature.profile.composable.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import com.acon.acon.core.designsystem.R -import com.acon.acon.core.designsystem.component.bottomsheet.AconBottomSheet -import com.acon.acon.core.designsystem.noRippleClickable -import com.acon.acon.core.designsystem.theme.AconTheme - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun GallerySelectBottomSheet( - isDefault: Boolean, - modifier : Modifier = Modifier, - onDismiss: () -> Unit = {}, - onGallerySelect: () -> Unit = {}, - onDefaultImageSelect: () -> Unit = {} -){ - val padding = if(!isDefault) 75.dp else 115.dp - AconBottomSheet( - onDismissRequest = { onDismiss() } - ) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(bottom = padding) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { - onGallerySelect() - onDismiss() - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start - ) { - Text( - text = stringResource(R.string.upload_photo_from_album), - color = AconTheme.color.White, - style = AconTheme.typography.Title4, - fontWeight = FontWeight.Normal, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 17.dp) - ) - } - if(!isDefault) { - Row( - modifier = Modifier - .fillMaxWidth() - .noRippleClickable { - onDefaultImageSelect() - onDismiss() - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start - ) { - Text( - text = stringResource(R.string.set_default_profile_image), - color = AconTheme.color.White, - style = AconTheme.typography.Title4, - fontWeight = FontWeight.Normal, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 17.dp) - ) - } - } - } - } -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/NicknameValidMessageRow.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/NicknameValidMessageRow.kt deleted file mode 100644 index b476afbde..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/NicknameValidMessageRow.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.acon.acon.feature.profile.composable.component - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.unit.dp -import com.acon.acon.core.designsystem.theme.AconTheme - -@Composable -fun NicknameValidMessageRow( - @StringRes validMessage: Int, - @DrawableRes iconRes: Int, - @StringRes validContentDescription: Int, - color: Color -){ - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 8.dp) - ) { - Icon( - imageVector = ImageVector.vectorResource(iconRes), - contentDescription = stringResource(validContentDescription), - tint = color, - modifier = Modifier.padding(vertical = 1.dp) - ) - Text( - text = stringResource(validMessage), - style = AconTheme.typography.Body1, - color = color, - modifier = Modifier - .padding(start = 4.dp) - ) - } -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfilePhotoBox.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfilePhotoBox.kt deleted file mode 100644 index 615a98fce..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfilePhotoBox.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.acon.acon.feature.profile.composable.component - -import android.net.Uri -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -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.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import coil3.compose.AsyncImage -import coil3.compose.rememberAsyncImagePainter -import com.acon.acon.core.designsystem.R -import com.acon.acon.core.designsystem.image.rememberDefaultLoadImageErrorPainter - -@Composable -fun ProfilePhotoBox( - photoUri: String, - modifier: Modifier = Modifier -) { - BoxWithConstraints( - modifier = modifier.fillMaxSize() - ) { - if (photoUri.isNotBlank()) { - val imageWidth = maxWidth - Box( - modifier = Modifier - .fillMaxSize() - .width(imageWidth) - .height(imageWidth) - .clip(CircleShape), - contentAlignment = Alignment.Center - ) { - when { - photoUri.startsWith("content://") -> { - Image( - painter = rememberAsyncImagePainter(Uri.parse(photoUri)), - contentDescription = stringResource(R.string.content_description_settings), - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - alignment = Alignment.Center - ) - } - - photoUri.startsWith("https://") -> { - AsyncImage( - model = photoUri, - contentDescription = stringResource(R.string.content_description_settings), - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - alignment = Alignment.Center, - error = rememberDefaultLoadImageErrorPainter() - ) - } - - photoUri == "basic_profile_image" -> { - Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_default_profile), - contentDescription = stringResource(R.string.content_description_default_profile_image), - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - alignment = Alignment.Center - ) - } - } - } - } else { - Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_default_profile), - contentDescription = stringResource(R.string.content_description_default_profile_image), - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, - alignment = Alignment.Center - ) - } - } -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfileTextField.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfileTextField.kt deleted file mode 100644 index aaa169b18..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/component/ProfileTextField.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.acon.acon.feature.profile.composable.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusManager -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.acon.acon.core.designsystem.R -import com.acon.acon.core.designsystem.noRippleClickable -import com.acon.acon.core.designsystem.theme.AconTheme -import com.acon.acon.feature.profile.composable.type.FocusType -import com.acon.acon.feature.profile.composable.type.TextFieldStatus - -@Composable -internal fun ProfileTextField( - status: TextFieldStatus, - focusType: FocusType, - focusRequester: FocusRequester, - modifier: Modifier = Modifier, - value: TextFieldValue = TextFieldValue(), - onValueChange: (TextFieldValue) -> Unit, - placeholder: String? = "", - isTyping: Boolean = false, - onFocusChanged: (Boolean, FocusType) -> Unit = { _, _ -> }, - visualTransformation: VisualTransformation = VisualTransformation.None, - onClick: () -> Unit = {}, -) { - Box( - modifier = modifier - .fillMaxWidth() - .border( - width = 1.dp, - color = AconTheme.color.GlassWhiteDefault, - shape = RoundedCornerShape(8.dp) - ) - .background( - shape = RoundedCornerShape(8.dp), - color = AconTheme.color.Gray900 - ) - .padding(vertical = 12.dp, horizontal = 10.dp) - .noRippleClickable(onClick = onClick), - contentAlignment = Alignment.CenterStart - ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - BasicTextField( - value = value, - onValueChange = onValueChange, - maxLines = 1, - cursorBrush = SolidColor(AconTheme.color.Action), - modifier = Modifier - .weight(1f) - .focusRequester(focusRequester) - .onFocusChanged { focusState -> - onFocusChanged(focusState.isFocused, focusType) - }, - textStyle = AconTheme.typography.Body1.copy( - color = AconTheme.color.White - ), - visualTransformation = visualTransformation, - decorationBox = { innerTextField -> - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.CenterStart - ) { - if (value.text.isEmpty() && status != TextFieldStatus.Focused) { - if (placeholder != null) { - Text( - text = placeholder, - style = AconTheme.typography.Body1, - color = AconTheme.color.Gray500 - ) - } - } - innerTextField() - } - } - ) - - if (value.text.isNotEmpty() && value.text.length <= 14) { - if (isTyping) { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp, - color = AconTheme.color.Gray6 - ) - } else { - Icon( - modifier = Modifier - .clickable { onValueChange(TextFieldValue()) } - .size(18.dp), - imageVector = ImageVector.vectorResource(R.drawable.ic_clear), - contentDescription = stringResource(R.string.clear_search_content_description), - tint = AconTheme.color.Gray50 - ) - } - } else { - Spacer(Modifier.size(18.dp)) - } - } - } -} - -fun Modifier.addFocusCleaner(focusManager: FocusManager, doOnClear: () -> Unit = {}): Modifier { - return this.pointerInput(Unit) { - detectTapGestures( - onTap = { - doOnClear() - focusManager.clearFocus() - } - ) - } -} - -@Preview -@Composable -private fun ProfileTextFieldPreview() { - AconTheme { - ProfileTextField( - status = TextFieldStatus.Inactive, - focusRequester = FocusRequester(), - focusType = FocusType.Nickname, - onValueChange = {}, - placeholder = "" - ) - } -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/MockSavedSpotList.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/MockSavedSpotList.kt deleted file mode 100644 index b39b75ac0..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/MockSavedSpotList.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.acon.acon.feature.profile.composable.screen - -import okhttp3.internal.immutableListOf - -internal val mockSpotList = immutableListOf( - com.acon.acon.core.model.model.profile.SavedSpotLegacy( - spotId = 1, - image = "https://acon-bucket.s3.ap-northeast-2.amazonaws.com/members/profile-images/e6547003-b4df-42fe-9275-9d9c5a008e79.jpg", - name = "카페 브리즈" - ), - com.acon.acon.core.model.model.profile.SavedSpotLegacy( - spotId = 2, - image = "", - name = "서울 맛집" - ), - com.acon.acon.core.model.model.profile.SavedSpotLegacy( - spotId = 3, - image = "https://acon-bucket.s3.ap-northeast-2.amazonaws.com/members/profile-images/e6547003-b4df-42fe-9275-9d9c5a008e79.jpg", - name = "초밥 천국" - ), - com.acon.acon.core.model.model.profile.SavedSpotLegacy( - spotId = 4, - image = "https://acon-bucket.s3.ap-northeast-2.amazonaws.com/members/profile-images/e6547003-b4df-42fe-9275-9d9c5a008e79.jpg", - name = "분식당" - ), - com.acon.acon.core.model.model.profile.SavedSpotLegacy( - spotId = 5, - image = "", - name = "핫플 베이커리" - ), - com.acon.acon.core.model.model.profile.SavedSpotLegacy( - spotId = 6, - image = "https://acon-bucket.s3.ap-northeast-2.amazonaws.com/members/profile-images/e6547003-b4df-42fe-9275-9d9c5a008e79.jpg", - name = "우식당" - ), - com.acon.acon.core.model.model.profile.SavedSpotLegacy( - spotId = 7, - image = "", - name = "얼음식당" - ), - com.acon.acon.core.model.model.profile.SavedSpotLegacy( - spotId = 8, - image = "https://acon-bucket.s3.ap-northeast-2.amazonaws.com/members/profile-images/e6547003-b4df-42fe-9275-9d9c5a008e79.jpg", - name = "소리식당" - ), - com.acon.acon.core.model.model.profile.SavedSpotLegacy( - spotId = 9, - image = "", - name = "얼음 베이커리" - ), -) \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt deleted file mode 100644 index 36c1d5442..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/ProfileViewModelLegacy.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.acon.acon.feature.profile.composable.screen.profile - -import androidx.compose.runtime.Immutable -import androidx.lifecycle.viewModelScope -import com.acon.acon.domain.repository.ProfileRepositoryLegacy -import com.acon.acon.core.model.type.SignInStatus -import com.acon.acon.core.ui.base.BaseContainerHost -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import org.orbitmvi.orbit.viewmodel.container -import javax.inject.Inject - -@HiltViewModel -class ProfileViewModel @Inject constructor( - private val profileRepositoryLegacy: ProfileRepositoryLegacy -) : BaseContainerHost() { - - val updateProfileState = profileRepositoryLegacy.getProfileType() - - override val container = - container(ProfileUiStateLegacy.Success(com.acon.acon.core.model.model.profile.ProfileInfoLegacy.Empty)) { - signInStatus.collect { - when(it) { - SignInStatus.GUEST -> reduce { ProfileUiStateLegacy.Guest } - else -> { - profileRepositoryLegacy.fetchProfile().collect { profileInfoResult -> - profileInfoResult.onSuccess { - reduce { ProfileUiStateLegacy.Success(profileInfoLegacy = it) } - }.onFailure { - postSideEffect(ProfileUiSideEffectLegacy.FailedToLoadProfileInfoLegacy) - } - } - } - } - } - } - - fun resetUpdateProfileType() { - viewModelScope.launch { - profileRepositoryLegacy.resetProfileType() - } - } - - fun onSpotDetail(spotId: Long) = intent { - postSideEffect(ProfileUiSideEffectLegacy.OnNavigateToSpotDetailScreen(spotId)) - } - - fun onBookmark() = intent { - postSideEffect(ProfileUiSideEffectLegacy.OnNavigateToBookmarkScreen) - } - - fun onSettings() = intent { - postSideEffect(ProfileUiSideEffectLegacy.OnNavigateToSettingsScreen) - } - - fun onEditProfile() = intent { - postSideEffect(ProfileUiSideEffectLegacy.OnNavigateToProfileEditScreenLegacy) - } -} - -sealed interface ProfileUiStateLegacy { - @Immutable - data class Success( - val profileInfoLegacy: com.acon.acon.core.model.model.profile.ProfileInfoLegacy - ) : ProfileUiStateLegacy - - data object Guest : ProfileUiStateLegacy -} - -sealed interface ProfileUiSideEffectLegacy { - data class OnNavigateToSpotDetailScreen(val spotId: Long) : ProfileUiSideEffectLegacy - data object OnNavigateToBookmarkScreen : ProfileUiSideEffectLegacy - data object OnNavigateToSpotListScreen : ProfileUiSideEffectLegacy - data object OnNavigateToSettingsScreen : ProfileUiSideEffectLegacy - data object OnNavigateToProfileEditScreenLegacy : ProfileUiSideEffectLegacy - - data object FailedToLoadProfileInfoLegacy : ProfileUiSideEffectLegacy -} diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkItemLegacy.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkItemLegacy.kt deleted file mode 100644 index 6c7a1c41f..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkItemLegacy.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.acon.acon.feature.profile.composable.screen.profile.composable - -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage -import com.acon.acon.core.designsystem.R -import com.acon.acon.core.designsystem.effect.imageGradientLayer -import com.acon.acon.core.designsystem.effect.imageGradientTopLayer -import com.acon.acon.core.designsystem.image.rememberDefaultLoadImageErrorPainter -import com.acon.acon.core.designsystem.theme.AconTheme - -@Composable -internal fun BookmarkItemLegacy( - spot: com.acon.acon.core.model.model.profile.SavedSpotLegacy, - onClickSpotItem:() -> Unit, - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .clip(RoundedCornerShape(8.dp)) - .clickable { onClickSpotItem() } - ) { - if(spot.image.isNotEmpty()) { - AsyncImage( - model = spot.image, - contentDescription = stringResource(R.string.store_background_image_content_description), - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .imageGradientTopLayer(), - error = rememberDefaultLoadImageErrorPainter() - ) - - Text( - text = if (spot.name.length > 9) spot.name.take(8) + stringResource(R.string.ellipsis) else spot.name, - color = AconTheme.color.White, - style = AconTheme.typography.Title5, - fontWeight = FontWeight.SemiBold, - modifier = Modifier - .align(Alignment.TopCenter) - .fillMaxWidth() - .padding(top = 20.dp) - .padding(horizontal = 20.dp) - ) - } else { - Image( - painter = painterResource(R.drawable.ic_bg_no_store_profile), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .imageGradientLayer() - ) - - Text( - text = if (spot.name.length > 9) spot.name.take(8) + stringResource(R.string.ellipsis) else spot.name, - color = AconTheme.color.White, - style = AconTheme.typography.Title5, - fontWeight = FontWeight.SemiBold, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.TopCenter) - .padding(top = 20.dp) - .padding(horizontal = 20.dp) - ) - - Text( - text = stringResource(R.string.no_store_image), - color = AconTheme.color.Gray50, - style = AconTheme.typography.Caption1, - modifier = Modifier - .align(Alignment.Center) - ) - } - } -} - -@Preview -@Composable -private fun BookmarkItemPreview() { - AconTheme { - BookmarkItemLegacy( - spot = com.acon.acon.core.model.model.profile.SavedSpotLegacy(1, "", ""), - onClickSpotItem = {} - ) - } -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt deleted file mode 100644 index baeb4cddf..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenContainerLegacy.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.acon.acon.feature.profile.composable.screen.profile.composable - -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel -import com.acon.acon.core.designsystem.R -import com.acon.acon.core.ui.android.showToast -import com.acon.acon.feature.profile.composable.screen.profile.ProfileUiSideEffectLegacy -import com.acon.acon.feature.profile.composable.screen.profile.ProfileViewModel -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import org.orbitmvi.orbit.compose.collectAsState -import org.orbitmvi.orbit.compose.collectSideEffect - -@Composable -fun ProfileScreenContainerLegacy( - snackbarHostState: SnackbarHostState, - modifier: Modifier = Modifier, - onNavigateToSpotDetailScreen: (Long) -> Unit = {}, - onNavigateToBookMarkScreen: () -> Unit = {}, - onNavigateToSpotListScreen: () -> Unit = {}, - onNavigateToSettingsScreen: () -> Unit = {}, - onNavigateToProfileEditScreen: () -> Unit = {}, - onNavigateToUploadScreen: () -> Unit = {}, - viewModel: ProfileViewModel = hiltViewModel() -) { - val context = LocalContext.current - val state by viewModel.collectAsState() - - val coroutineScope = rememberCoroutineScope() - val snackbarMsg = stringResource(R.string.profile_save_success) - - LaunchedEffect(viewModel.updateProfileState) { - viewModel.updateProfileState.collectLatest { - when(it) { - com.acon.acon.core.model.type.UpdateProfileType.IDLE -> {} - com.acon.acon.core.model.type.UpdateProfileType.SUCCESS -> { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = snackbarMsg, - duration = SnackbarDuration.Short, - ) - } - viewModel.resetUpdateProfileType() - } - com.acon.acon.core.model.type.UpdateProfileType.FAILURE -> {} - } - } - } - - ProfileScreenLegacy( - state = state, - modifier = modifier, - onBookmark = viewModel::onBookmark, - onSettings = viewModel::onSettings, - onSpotDetail = viewModel::onSpotDetail, - onEditProfile = viewModel::onEditProfile, - onNavigateToSpotListScreen = onNavigateToSpotListScreen, - onNavigateToUploadScreen = onNavigateToUploadScreen - ) - - viewModel.useSignInStatus() - viewModel.collectSideEffect { - when(it) { - is ProfileUiSideEffectLegacy.OnNavigateToSpotDetailScreen -> { onNavigateToSpotDetailScreen(it.spotId) } - is ProfileUiSideEffectLegacy.OnNavigateToBookmarkScreen -> { onNavigateToBookMarkScreen() } - is ProfileUiSideEffectLegacy.OnNavigateToSpotListScreen -> { onNavigateToSpotListScreen() } - is ProfileUiSideEffectLegacy.OnNavigateToSettingsScreen -> { onNavigateToSettingsScreen() } - is ProfileUiSideEffectLegacy.OnNavigateToProfileEditScreenLegacy -> { onNavigateToProfileEditScreen() } - is ProfileUiSideEffectLegacy.FailedToLoadProfileInfoLegacy -> { context.showToast(R.string.unknown_error) } - } - } -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt deleted file mode 100644 index 24d04ac90..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/ProfileScreenLegacy.kt +++ /dev/null @@ -1,346 +0,0 @@ -package com.acon.acon.feature.profile.composable.screen.profile.composable - -import android.annotation.SuppressLint -import androidx.compose.foundation.Image -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.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -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.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage -import com.acon.acon.core.designsystem.R -import com.acon.acon.core.designsystem.component.bottombar.AconBottomBar -import com.acon.acon.core.designsystem.component.bottombar.BottomNavType -import com.acon.acon.core.designsystem.component.topbar.AconTopBar -import com.acon.acon.core.designsystem.effect.LocalHazeState -import com.acon.acon.core.designsystem.effect.defaultHazeEffect -import com.acon.acon.core.designsystem.noRippleClickable -import com.acon.acon.core.designsystem.theme.AconTheme -import com.acon.acon.feature.profile.composable.screen.profile.ProfileUiStateLegacy -import com.acon.acon.core.ui.compose.LocalRequestSignIn -import com.acon.acon.core.ui.compose.LocalSignInStatus -import com.acon.acon.core.ui.compose.getScreenHeight -import dev.chrisbanes.haze.hazeSource - -@SuppressLint("ConfigurationScreenWidthHeight") -@Composable -fun ProfileScreenLegacy( - state: ProfileUiStateLegacy, - modifier: Modifier = Modifier, - onSpotDetail: (Long) -> Unit = {}, - onBookmark: () -> Unit = {}, - onSettings: () -> Unit = {}, - onEditProfile: () -> Unit = {}, - onNavigateToSpotListScreen: () -> Unit = {}, - onNavigateToUploadScreen: () -> Unit = {}, -) { - val screenHeightDp = getScreenHeight() - val profileImageHeight = (screenHeightDp * (60f / 740f)) - val admobHeight = (screenHeightDp * (165f / 740f)) - val savedStoreHeight = (screenHeightDp * (200f / 740f)) - - val userType = LocalSignInStatus.current - val onSignInRequired = LocalRequestSignIn.current - - Column(modifier) { - when (state) { - is ProfileUiStateLegacy.Success -> { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .verticalScroll(rememberScrollState()) - .hazeSource(LocalHazeState.current) - ) { - AconTopBar( - content = { - Text( - text = stringResource(R.string.profile_topbar), - style = AconTheme.typography.Title4, - fontWeight = FontWeight.SemiBold, - color = AconTheme.color.White - ) - }, - trailingIcon = { - IconButton( - onClick = { onSettings() } - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_setting), - contentDescription = stringResource(R.string.content_description_settings), - tint = AconTheme.color.White - ) - } - }, - modifier = Modifier.padding(vertical = 14.dp) - ) - - Column( - modifier = Modifier - .padding(top = 40.dp) - .padding(horizontal = 16.dp) - ) { - Row { - if (state.profileInfoLegacy.image.isEmpty()) { - Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_default_profile), - contentDescription = stringResource(R.string.content_description_default_profile_image), - modifier = Modifier - .size(profileImageHeight) - .clip(CircleShape) - ) - } else { - AsyncImage( - model = state.profileInfoLegacy.image, - contentDescription = stringResource(R.string.content_description_profile_image), - modifier = Modifier - .size(profileImageHeight) - .clip(CircleShape), - contentScale = ContentScale.Crop, - error = painterResource(R.drawable.ic_default_profile) - ) - } - - Column( - modifier = Modifier - .padding(vertical = 5.dp) - .padding(start = 16.dp) - ) { - Text( - text = state.profileInfoLegacy.nickname, - style = AconTheme.typography.Headline4, - color = AconTheme.color.White - ) - - Spacer(Modifier.height(4.dp)) - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.edit_profile), - style = AconTheme.typography.Body1, - color = AconTheme.color.Gray500, - ) - - Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_edit), - contentDescription = stringResource(R.string.content_description_edit_profile), - modifier = Modifier - .padding(start = 4.dp) - .noRippleClickable { onEditProfile() } - ) - } - } - } - - // TODO - saveSpot = isEmpty -> 저장한 장소가 없어요. - - Spacer(Modifier.height(42.dp)) - if (state.profileInfoLegacy != com.acon.acon.core.model.model.profile.ProfileInfoLegacy.Empty) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 6.dp) - ) { - Text( - text = stringResource(R.string.saved_store), - color = AconTheme.color.White, - style = AconTheme.typography.Title4, - fontWeight = FontWeight.SemiBold - ) - - Spacer(Modifier.weight(1f)) - if (state.profileInfoLegacy.savedSpotLegacies.isNotEmpty()) { - Text( - text = stringResource(R.string.show_saved_all_store), - color = AconTheme.color.Action, - style = AconTheme.typography.Body1, - fontWeight = FontWeight.SemiBold, - modifier = Modifier - .padding(vertical = 2.dp) - .padding(end = 8.dp) - .noRippleClickable { onBookmark() } - ) - } - } - - Spacer(Modifier.height(8.dp)) - if (state.profileInfoLegacy.savedSpotLegacies.isNotEmpty()) { - LazyRow( - modifier = Modifier - .fillMaxWidth() - .height(savedStoreHeight), - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { - items( - items = state.profileInfoLegacy.savedSpotLegacies, - key = { it.spotId } - ) { spot -> - BookmarkItemLegacy( - spot = spot, - onClickSpotItem = { onSpotDetail(spot.spotId) }, - modifier = Modifier.aspectRatio(150f / 217f) - ) - } - } - } else { - Text( - text = stringResource(R.string.no_saved_spot), - style = AconTheme.typography.Body1, - fontWeight = FontWeight.W400, - color = AconTheme.color.Gray500, - ) - } - } - Spacer(Modifier.height(if (state.profileInfoLegacy.savedSpotLegacies.isEmpty()) 40.dp else 20.dp)) -// ProfileNativeAd( -// screenHeight = admobHeight, -// modifier = Modifier.padding(bottom = 23.dp) -// ) - } - } - } - - is ProfileUiStateLegacy.Guest -> { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .verticalScroll(rememberScrollState()) - .hazeSource(LocalHazeState.current) - ) { - AconTopBar( - content = { - Text( - text = stringResource(R.string.profile_topbar), - style = AconTheme.typography.Title4, - fontWeight = FontWeight.SemiBold, - color = AconTheme.color.White - ) - }, - trailingIcon = { - IconButton( - onClick = { onSettings() } - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_setting), - contentDescription = stringResource(R.string.content_description_settings), - tint = AconTheme.color.White - ) - } - }, - modifier = Modifier.padding(vertical = 14.dp) - ) - - Column( - modifier = Modifier - .weight(1f) - .padding(top = 40.dp) - .padding(horizontal = 16.dp) - - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_default_profile), - contentDescription = stringResource(R.string.content_description_default_profile_image), - modifier = Modifier - .size(profileImageHeight) - .clip(CircleShape) - ) - - Row( - modifier = Modifier - .padding(start = 16.dp) - .noRippleClickable { - onSignInRequired("") - } - ) { - Text( - text = stringResource(R.string.you_need_sign_in), - style = AconTheme.typography.Headline4, - color = AconTheme.color.White, - ) - - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_right_24), - contentDescription = stringResource(R.string.content_description_go_sign_in), - modifier = Modifier.padding(start = 4.dp), - tint = AconTheme.color.Gray50 - ) - } - } - -// ProfileNativeAd( -// screenHeight = admobHeight, -// modifier = Modifier.padding(top = 20.dp) -// ) - } - } - } - } - AconBottomBar( - selectedItem = BottomNavType.PROFILE, - onItemClick = { bottomType -> - when (bottomType) { - BottomNavType.SPOT -> { - onNavigateToSpotListScreen() - } - - BottomNavType.UPLOAD -> { - if (userType == com.acon.acon.core.model.type.SignInStatus.GUEST) { - onSignInRequired("click_upload_guest?") - } else { - onNavigateToUploadScreen() - } - } - - BottomNavType.PROFILE -> Unit - } - }, - modifier = Modifier - .fillMaxWidth() - .defaultHazeEffect( - hazeState = LocalHazeState.current, - tintColor = AconTheme.color.GlassGray900 - ) - .navigationBarsPadding() - ) - } -} - -@Preview -@Composable -private fun ProfileScreenPreview() { - AconTheme { - ProfileScreenLegacy( - state = ProfileUiStateLegacy.Guest - ) - } -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModelLegacy.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModelLegacy.kt deleted file mode 100644 index b54914d37..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/ProfileModViewModelLegacy.kt +++ /dev/null @@ -1,423 +0,0 @@ -package com.acon.acon.feature.profile.composable.screen.profileMod - -import android.app.Application -import android.net.Uri -import androidx.compose.runtime.Immutable -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import com.acon.acon.domain.error.profile.ValidateNicknameErrorLegacy -import com.acon.acon.domain.repository.ProfileRepositoryLegacy -import com.acon.acon.feature.profile.BuildConfig -import com.acon.acon.feature.profile.composable.type.BirthdayValidationStatus -import com.acon.acon.feature.profile.composable.type.FocusType -import com.acon.acon.feature.profile.composable.type.NicknameErrorType -import com.acon.acon.feature.profile.composable.type.NicknameValidationStatus -import com.acon.acon.feature.profile.composable.type.TextFieldStatus -import com.acon.acon.feature.profile.composable.utils.isAllowedChar -import com.acon.acon.feature.profile.composable.utils.limitedNickname -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import org.orbitmvi.orbit.ContainerHost -import org.orbitmvi.orbit.annotation.OrbitExperimental -import org.orbitmvi.orbit.viewmodel.container -import timber.log.Timber -import javax.inject.Inject - -@OptIn(OrbitExperimental::class) -@HiltViewModel -class ProfileModViewModelLegacy @Inject constructor( - private val profileRepositoryLegacy: ProfileRepositoryLegacy, - application: Application -) : AndroidViewModel(application), ContainerHost { - - private var nicknameValidationJob: Job? = null - - override val container = - container(ProfileModStateLegacy.Loading) { - fetchUserProfileInfo() - } - - fun onFocusChanged(isFocused: Boolean, field: FocusType) = intent { - runOn { - reduce { - when (field) { - FocusType.Nickname -> state.copy( - nicknameFieldStatus = if (isFocused) TextFieldStatus.Focused else TextFieldStatus.Inactive - ) - - FocusType.Birthday -> state.copy( - birthdayFieldStatus = if (isFocused) TextFieldStatus.Focused else TextFieldStatus.Inactive - ) - } - } - } - } - - private fun fetchUserProfileInfo() = intent { - profileRepositoryLegacy.fetchProfile().collect { - it.onSuccess { profile -> - reduce { - ProfileModStateLegacy.Success( - fetchedNickname = profile.nickname, - fetchedBirthday = profile.birthDate?.filter { it.isDigit() } ?: "", - fetchedPhotoUri = profile.image - ) - } - onNicknameChanged(profile.nickname, delayValidation = true) - onBirthdayChanged(profile.birthDate ?: "") - } - } - } - - private suspend fun validateNickname( - nickname: String - ): NicknameErrorType? { - return profileRepositoryLegacy.validateNickname(nickname) - .map { null } - .recover { throwable -> - when (throwable) { - is ValidateNicknameErrorLegacy.UnsatisfiedCondition -> NicknameErrorType.Invalid - is ValidateNicknameErrorLegacy.AlreadyUsedNickname -> NicknameErrorType.Duplicate - else -> null - } - } - .getOrNull() - } - - fun onNicknameChanged(text: String, delayValidation: Boolean = false) = intent { - runOn { - val (limitedText, count) = text.limitedNickname() - - val localErrorType: NicknameErrorType? = when { - text.any { !it.isAllowedChar() } -> NicknameErrorType.Invalid - else -> null - } - - reduce { - state.copy( - nickname = limitedText, - isEdited = (state.isEdited || limitedText != state.fetchedNickname), - nicknameCount = count, - nicknameValidationStatus = when { - text.isEmpty() -> NicknameValidationStatus.Empty - localErrorType != null -> NicknameValidationStatus.Error(localErrorType) - else -> NicknameValidationStatus.Typing - }, - nicknameFieldStatus = if (limitedText.isEmpty()) TextFieldStatus.Empty else state.nicknameFieldStatus - ) - } - - nicknameValidationJob?.cancel() - nicknameValidationJob = viewModelScope.launch { - if (text.length > 14) return@launch - if (delayValidation) delay(1000L) else delay(500L) - - if (localErrorType == NicknameErrorType.Invalid) return@launch - val serverErrorType = validateNickname(limitedText) - - reduce { - state.copy( - nicknameValidationStatus = when { - limitedText.isBlank() -> NicknameValidationStatus.Empty - serverErrorType != null -> NicknameValidationStatus.Error( - serverErrorType - ) - - localErrorType != null -> NicknameValidationStatus.Error(localErrorType) - else -> NicknameValidationStatus.Valid - } - ) - } - } - } - } - - fun onBirthdayChanged(text: String) = intent { - runOn { - val digitsOnly = text.filter { it.isDigit() } - val limitedDigits = digitsOnly.take(8) - - val (validationStatus, fieldStatus) = when { - limitedDigits.isEmpty() -> BirthdayValidationStatus.Empty to TextFieldStatus.Empty - limitedDigits.length < 8 -> BirthdayValidationStatus.Empty to TextFieldStatus.Focused - else -> { - val isValid = validateBirthday(limitedDigits) - (if (isValid) BirthdayValidationStatus.Valid else BirthdayValidationStatus.Invalid) to - (if (isValid) TextFieldStatus.Focused else TextFieldStatus.Error) - } - } - - reduce { - state.copy( - isEdited = state.isEdited || limitedDigits != state.fetchedBirthday, - birthday = limitedDigits, - birthdayValidationStatus = validationStatus, - birthdayFieldStatus = fieldStatus - ) - } - } - } - - private fun validateBirthday(birthday: String): Boolean { - val today = java.time.LocalDate.now() - val year = birthday.substring(0, 4).toIntOrNull() ?: return false - val month = birthday.substring(4, 6).toIntOrNull() ?: return false - val day = birthday.substring(6, 8).toIntOrNull() ?: return false - - val currentYear = java.time.Year.now().value - if (year > currentYear || year <= 1940) return false - - if (month !in 1..12) return false - - val maxDays = java.time.YearMonth.of(year, month).lengthOfMonth() - if (day !in 1..maxDays) return false - - return try { - val inputDate = java.time.LocalDate.of(year, month, day) - !inputDate.isAfter(today) - } catch (e: Exception) { - false - } - } - - fun navigateToBack() = intent { - postSideEffect(ProfileModSideEffectLegacy.NavigateBack) - } - - fun onRequestExitDialog() = intent { - runOn { - reduce { - state.copy(showExitDialog = true) - } - } - } - - fun onDisMissExitDialog() = intent { - runOn { - reduce { - state.copy(showExitDialog = false) - } - } - } - - fun updateProfileImage(selectedPhotoUri: String) = intent { - runOn { - reduce { - state.copy(selectedPhotoUri = selectedPhotoUri) - } - } - } - - fun onRequestProfileEditModal() = intent { - runOn { - reduce { - state.copy(showPhotoEditModal = true) - } - } - } - - fun onDisMissProfileEditModal() = intent { - runOn { - reduce { - state.copy(showPhotoEditModal = false) - } - } - } - - fun getPreSignedUrl() = intent { - runOn { - Timber.tag(TAG).d("getPreSignedUrl() 호출됨") - val nickname = state.nickname - val isBirthdayValid = state.birthdayValidationStatus == BirthdayValidationStatus.Valid - val birthday = if (isBirthdayValid) state.birthday else null - - when { - state.selectedPhotoUri.isEmpty() -> { - Timber.tag(TAG).d("selectedPhotoUri가 비어 있음 → 기존 이미지로 updateProfile 호출") - updateProfile( - fileName = state.fetchedPhotoUri, - nickname = nickname, - birthday = birthday, - uri = state.fetchedPhotoUri - ) - } - - state.selectedPhotoUri == "basic_profile_image" -> { - Timber.tag(TAG).d("selectedPhotoUri가 basic_profile_image → 기본 이미지로 updateProfile 호출") - updateProfile( - fileName = state.uploadFileName, - nickname = nickname, - birthday = birthday, - uri = state.selectedPhotoUri - ) - } - - else -> { - Timber.tag(TAG).d("selectedPhotoUri가 커스텀 이미지 → presigned URL 요청") - profileRepositoryLegacy.getPreSignedUrl() - .onSuccess { result -> - reduce { state.copy(uploadFileName = result.fileName) } - putPhotoToPreSignedUrl( - nickname = nickname, - birthday = birthday, - imageUri = Uri.parse(state.selectedPhotoUri), - preSignedUrl = result.preSignedUrl - ) - } - .onFailure { - Timber.tag(TAG).e(it, "presigned URL 획득 실패") - } - } - } - } - } - - private fun putPhotoToPreSignedUrl( - nickname: String, - birthday: String?, - imageUri: Uri, - preSignedUrl: String - ) = intent { - runOn { - val context = getApplication().applicationContext - val client = OkHttpClient() - - try { - val byteArray: ByteArray - val mimeType: String - - if (state.selectedPhotoUri.startsWith("content://")) { - val inputStream = context.contentResolver.openInputStream(imageUri) - byteArray = inputStream?.readBytes() - ?: throw IllegalArgumentException("이미지 읽기 실패") - mimeType = context.contentResolver.getType(imageUri) ?: "image/jpeg" - - } else if (state.selectedPhotoUri.startsWith("http://") || state.selectedPhotoUri.startsWith("https://")) { - Timber.tag(TAG).d("원격 URL에서 이미지 가져오기 시작") - val getRequest = Request.Builder().url(imageUri.toString()).build() - val getResponse = client.newCall(getRequest).execute() - - if (!getResponse.isSuccessful) { - Timber.tag(TAG).e("원격 이미지 가져오기 실패, code: %d", getResponse.code) - throw IllegalArgumentException("원격 이미지 가져오기 실패") - } - - byteArray = getResponse.body?.bytes() - ?: throw IllegalArgumentException("원격 이미지 읽기 실패") - mimeType = getResponse.header("Content-Type") ?: "image/jpeg" - - } else { - Timber.tag(TAG).e("지원하지 않는 URI scheme: %s", state.selectedPhotoUri) - throw IllegalArgumentException("지원하지 않는 URI scheme") - } - - val fileBody = - byteArray.toRequestBody(mimeType.toMediaTypeOrNull(), 0, byteArray.size) - - val request = Request.Builder() - .url(preSignedUrl) - .put(fileBody) - .addHeader("Content-Type", mimeType) - .build() - - val response = client.newCall(request).execute() - val bucketImageUri = "${BuildConfig.BUCKET_URL}${state.uploadFileName}" - - if (response.isSuccessful) { - Timber.tag(TAG).d("이미지 업로드 성공") - if (state.birthdayValidationStatus == BirthdayValidationStatus.Valid) { - updateProfile( - fileName = state.uploadFileName, - nickname = nickname, - birthday = birthday, - uri = bucketImageUri - ) - } else { - updateProfile( - fileName = state.uploadFileName, - nickname = nickname, - birthday = null, - uri = bucketImageUri - ) - } - } else { - Timber.tag(TAG).e("이미지 업로드 실패, code: %d", response.code) - } - } catch (e: Exception) { - Timber.tag(TAG).e(e, "이미지 업로드 과정에서 예외 발생: %s", e.message) - } - } - } - - private fun updateProfile(fileName: String, nickname: String, birthday: String?, uri: String) = - intent { - profileRepositoryLegacy.updateProfile(fileName, nickname, birthday, uri) - .onSuccess { - profileRepositoryLegacy.updateProfileType(com.acon.acon.core.model.type.UpdateProfileType.SUCCESS) - postSideEffect(ProfileModSideEffectLegacy.NavigateToProfileLegacy) - } - .onFailure { - profileRepositoryLegacy.updateProfileType(com.acon.acon.core.model.type.UpdateProfileType.FAILURE) - postSideEffect(ProfileModSideEffectLegacy.NavigateToProfileLegacy) - } - } - - companion object { - const val TAG = "ProfileViewModel" - } -} - -sealed interface ProfileModStateLegacy { - @Immutable - data class Success( - val fetchedNickname: String = "", - val nickname: String = "", - val nicknameFieldStatus: TextFieldStatus = TextFieldStatus.Inactive, - val nicknameValidationStatus: NicknameValidationStatus = NicknameValidationStatus.Empty, - val nicknameCount: Int = 0, - - val fetchedBirthday: String = "", - val birthday: String = "", - val birthdayFieldStatus: TextFieldStatus = TextFieldStatus.Inactive, - val birthdayValidationStatus: BirthdayValidationStatus = BirthdayValidationStatus.Empty, - - val fetchedPhotoUri: String = "", - val selectedPhotoUri: String = "", - val uploadFileName: String = "", - - val isEdited: Boolean = false, - val showExitDialog: Boolean = false, - val showPhotoEditModal: Boolean = false, - ) : ProfileModStateLegacy { - val isEditButtonEnabled: Boolean - get() { - val isProfileImageChanged = when { - selectedPhotoUri.contains("basic_profile_image") && - fetchedPhotoUri.contains("basic_profile_image") -> false - - selectedPhotoUri.isNotEmpty() && selectedPhotoUri != fetchedPhotoUri -> true - else -> false - } - val isBirthValid = fetchedBirthday.isNotEmpty() && birthday.isEmpty() - val isContentValid = nicknameValidationStatus == NicknameValidationStatus.Valid && - (birthday.isEmpty() || birthdayValidationStatus == BirthdayValidationStatus.Valid) - - return (isProfileImageChanged && isContentValid) || isBirthValid || (isEdited && isContentValid) - } - } - - data object Loading : ProfileModStateLegacy - data object LoadFailed : ProfileModStateLegacy -} - -sealed interface ProfileModSideEffectLegacy { - data object NavigateBack : ProfileModSideEffectLegacy - data class UpdateProfileImageLegacy(val imageUri: String?) : ProfileModSideEffectLegacy - data object NavigateToProfileLegacy : ProfileModSideEffectLegacy -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenContainerLegacy.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenContainerLegacy.kt deleted file mode 100644 index 9d61a7f1d..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenContainerLegacy.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.acon.acon.feature.profile.composable.screen.profileMod.composable - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel -import com.acon.acon.feature.profile.composable.screen.profileMod.ProfileModSideEffectLegacy -import com.acon.acon.feature.profile.composable.screen.profileMod.ProfileModViewModelLegacy -import org.orbitmvi.orbit.compose.collectAsState -import org.orbitmvi.orbit.compose.collectSideEffect - -@Composable -fun ProfileModScreenContainerLegacy( - modifier: Modifier = Modifier, - viewModel: ProfileModViewModelLegacy = hiltViewModel(), - selectedPhotoId: String? = null, - onNavigateToBack: () -> Unit = {}, - onClickComplete: () -> Unit = {}, -) { - val state by viewModel.collectAsState() - - LaunchedEffect(selectedPhotoId) { - if (selectedPhotoId?.isNotEmpty() == true) { - viewModel.updateProfileImage(selectedPhotoId) - } - } - - viewModel.collectSideEffect { effect -> - when (effect) { - is ProfileModSideEffectLegacy.NavigateBack -> { - onNavigateToBack() - } - - is ProfileModSideEffectLegacy.UpdateProfileImageLegacy -> { - selectedPhotoId.let { - viewModel.updateProfileImage(selectedPhotoId ?: "") - } - } - - is ProfileModSideEffectLegacy.NavigateToProfileLegacy -> { - onClickComplete() - } - } - } - - ProfileModScreenLegacy( - modifier = modifier, - state = state, - navigateToBack = viewModel::navigateToBack, - onNicknameChanged = viewModel::onNicknameChanged, - onBirthdayChanged = viewModel::onBirthdayChanged, - onFocusChanged = viewModel::onFocusChanged, - onRequestExitDialog = viewModel::onRequestExitDialog, - onDisMissExitDialog = viewModel::onDisMissExitDialog, - onRequestProfileEditModal = viewModel::onRequestProfileEditModal, - onDisMissProfileEditModal = viewModel::onDisMissProfileEditModal, - onUpdateProfileImage = viewModel::updateProfileImage, - onClickSaveButton = viewModel::getPreSignedUrl - ) -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenLegacy.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenLegacy.kt deleted file mode 100644 index be903f021..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profileMod/composable/ProfileModScreenLegacy.kt +++ /dev/null @@ -1,449 +0,0 @@ -package com.acon.acon.feature.profile.composable.screen.profileMod.composable - -import androidx.activity.compose.BackHandler -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.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.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.acon.acon.core.designsystem.R -import com.acon.acon.core.designsystem.component.button.v2.AconFilledButton -import com.acon.acon.core.designsystem.component.dialog.v2.AconTwoActionDialog -import com.acon.acon.core.designsystem.component.topbar.AconTopBar -import com.acon.acon.core.designsystem.effect.LocalHazeState -import com.acon.acon.core.designsystem.noRippleClickable -import com.acon.acon.core.designsystem.theme.AconTheme -import com.acon.acon.feature.profile.composable.component.GallerySelectBottomSheet -import com.acon.acon.feature.profile.composable.component.NicknameValidMessageRow -import com.acon.acon.feature.profile.composable.component.ProfilePhotoBox -import com.acon.acon.feature.profile.composable.component.ProfileTextField -import com.acon.acon.feature.profile.composable.component.addFocusCleaner -import com.acon.acon.feature.profile.composable.screen.profileMod.ProfileModStateLegacy -import com.acon.acon.feature.profile.composable.type.BirthdayValidationStatus -import com.acon.acon.feature.profile.composable.type.FocusType -import com.acon.acon.feature.profile.composable.type.NicknameValidationStatus -import com.acon.acon.feature.profile.composable.type.contentDescriptionResId -import com.acon.acon.feature.profile.composable.type.validMessageResId -import com.acon.acon.feature.profile.composable.utils.BirthdayTransformation -import com.acon.acon.core.ui.compose.getScreenHeight -import com.acon.acon.core.ui.compose.getScreenWidth -import dev.chrisbanes.haze.hazeSource - -@Composable -internal fun ProfileModScreenLegacy( - modifier: Modifier = Modifier, - state: ProfileModStateLegacy, - navigateToBack: () -> Unit, - onNicknameChanged: (String) -> Unit = {}, - onBirthdayChanged: (String) -> Unit = {}, - onFocusChanged: (Boolean, FocusType) -> Unit = { _, _ -> }, - onRequestExitDialog: () -> Unit, - onDisMissExitDialog: () -> Unit, - onRequestProfileEditModal: () -> Unit, - onDisMissProfileEditModal: () -> Unit, - onUpdateProfileImage: (String) -> Unit, - onClickSaveButton: () -> Unit = {}, -) { - val context = LocalContext.current - val screenWidthDp = getScreenWidth() - val screenHeightDp = getScreenHeight() - val dialogWidth = (screenWidthDp * (260f / 360f)) - val profileImageHeight = (screenHeightDp * (80f / 740f)) - - val photoPickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.PickVisualMedia() - ) { uri -> - uri?.let { - onUpdateProfileImage(uri.toString()) - onDisMissProfileEditModal() - } - } - - val focusManager = LocalFocusManager.current - - val nickNameFocusRequester = remember { FocusRequester() } - val birthDayFocusRequester = remember { FocusRequester() } - - when (state) { - ProfileModStateLegacy.LoadFailed -> {} - ProfileModStateLegacy.Loading -> {} - is ProfileModStateLegacy.Success -> { - - BackHandler(enabled = true) { - if(state.isEdited) { - onRequestExitDialog() - } else { - navigateToBack() - } - } - - var nicknameTextFieldValue by rememberSaveable( - state.fetchedNickname, - stateSaver = TextFieldValue.Saver - ) { - mutableStateOf(TextFieldValue(state.fetchedNickname)) - } - - var birthdayTextFieldValue by rememberSaveable( - state.fetchedBirthday, - stateSaver = TextFieldValue.Saver - ) { - mutableStateOf(TextFieldValue(state.fetchedBirthday)) - } - - if (state.showPhotoEditModal) { - GallerySelectBottomSheet( - isDefault = when { - state.selectedPhotoUri.isNotEmpty() -> { - state.selectedPhotoUri.contains("basic_profile_image") - } - - else -> { - state.fetchedPhotoUri.contains("basic_profile_image") - } - }, - onDismiss = { onDisMissProfileEditModal() }, - onGallerySelect = { - onDisMissProfileEditModal() - photoPickerLauncher.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) - ) - }, - onDefaultImageSelect = { onUpdateProfileImage("basic_profile_image") }, - ) - } - - if (state.showExitDialog) { - AconTwoActionDialog( - title = stringResource(R.string.profile_mod_exit_title), - action1 = stringResource(R.string.continue_writing), - action2 = stringResource(R.string.exit), - onDismissRequest = { - onRequestExitDialog() - }, - onAction1 = { - onDisMissExitDialog() - }, - onAction2 = { - navigateToBack() - }, - modifier = Modifier.width(dialogWidth) - ) - } - - Column( - modifier = modifier - .fillMaxSize() - .background(color = AconTheme.color.Gray900) - .hazeSource(LocalHazeState.current) - .statusBarsPadding() - .navigationBarsPadding() - .addFocusCleaner(focusManager), - verticalArrangement = Arrangement.Center - ) { - AconTopBar( - leadingIcon = { - IconButton( - onClick = { - if(state.isEdited) { onRequestExitDialog() } - else { navigateToBack() } - } - ) { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_topbar_arrow_left), - contentDescription = stringResource(R.string.back), - tint = AconTheme.color.Gray50 - ) - } - }, - content = { - Text( - text = stringResource(R.string.profile_edit_topbar), - style = AconTheme.typography.Title4, - fontWeight = FontWeight.SemiBold, - color = AconTheme.color.White - ) - }, - modifier = Modifier.padding(vertical = 14.dp) - ) - - Box( - modifier = Modifier.weight(1f) - ) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 19.dp), - horizontalArrangement = Arrangement.Center - ) { - Spacer(modifier = Modifier.weight(1f)) - Box( - modifier = Modifier - .size(profileImageHeight) - .aspectRatio(1f) - .noRippleClickable { onRequestProfileEditModal() }, - contentAlignment = Alignment.Center - ) { - ProfilePhotoBox( - modifier = Modifier - .fillMaxSize() - .align(Alignment.Center), - photoUri = state.selectedPhotoUri.ifEmpty { state.fetchedPhotoUri } - ) - Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_profile_img_edit), - contentDescription = stringResource(R.string.content_description_edit_profile), - modifier = Modifier - .align(alignment = Alignment.BottomEnd) - ) - } - Spacer(modifier = Modifier.weight(1f)) - } - - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 48.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start - ) { - Text( - text = stringResource(R.string.nickname_textfield_title), - style = AconTheme.typography.Title4, - fontWeight = FontWeight.SemiBold, - color = AconTheme.color.White - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = stringResource(R.string.star), - style = AconTheme.typography.Title4, - fontWeight = FontWeight.SemiBold, - color = AconTheme.color.Gray50 - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - ProfileTextField( - status = state.nicknameFieldStatus, - focusType = FocusType.Nickname, - focusRequester = nickNameFocusRequester, - value = nicknameTextFieldValue, - isTyping = (state.nicknameValidationStatus == NicknameValidationStatus.Typing), - onValueChange = { fieldValue -> - val lowerCaseText = fieldValue.text.lowercase() - if (lowerCaseText.length <= 14) { - nicknameTextFieldValue = - fieldValue.copy(text = lowerCaseText) - onNicknameChanged(lowerCaseText) - } - }, - onFocusChanged = onFocusChanged, - onClick = { - nickNameFocusRequester.requestFocus() - } - ) - - Spacer(modifier = Modifier.height(4.dp)) - Row( - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.Start - ) { - Box( - Modifier.height(24.dp) - ) { - when (state.nicknameValidationStatus) { - is NicknameValidationStatus.Empty -> { - NicknameValidMessageRow( - validMessage = R.string.nickname_error_empty, - iconRes = R.drawable.ic_error, - validContentDescription = R.string.content_description_empty_nickname, - color = AconTheme.color.Danger - ) - } - - is NicknameValidationStatus.Error -> { - val validState = state.nicknameValidationStatus - NicknameValidMessageRow( - validMessage = validState.errorTypes.validMessageResId(), - iconRes = R.drawable.ic_error, - validContentDescription = validState.errorTypes.contentDescriptionResId(), - color = AconTheme.color.Danger - ) - } - - is NicknameValidationStatus.Valid -> { - NicknameValidMessageRow( - validMessage = R.string.nickname_valid, - iconRes = R.drawable.ic_valid, - validContentDescription = R.string.content_description_valid_nickname, - color = AconTheme.color.Success - ) - } - - else -> Unit - } - } - } - - Row( - modifier = Modifier.padding(top = 5.dp, end = 8.dp), - horizontalArrangement = Arrangement.End - ) { - Text( - text = "${state.nicknameCount}", - style = AconTheme.typography.Caption1, - color = AconTheme.color.Gray500 - ) - Text( - text = stringResource(R.string.max_nickname_count), - style = AconTheme.typography.Caption1, - color = AconTheme.color.Gray500 - ) - } - } - } - - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 44.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start - ) { - Text( - text = stringResource(R.string.birthdate_field_title), - style = AconTheme.typography.Title4, - color = AconTheme.color.White, - fontWeight = FontWeight.SemiBold - ) - Spacer(modifier = Modifier.width(4.dp)) - } - Spacer(modifier = Modifier.height(12.dp)) - ProfileTextField( - status = state.birthdayFieldStatus, - focusType = FocusType.Birthday, - focusRequester = birthDayFocusRequester, - value = birthdayTextFieldValue, - placeholder = stringResource(R.string.birthday_placeholder), - onValueChange = { fieldValue -> - if (fieldValue.text.length <= 8) { - birthdayTextFieldValue = fieldValue - onBirthdayChanged(fieldValue.text) - } - }, - onFocusChanged = onFocusChanged, - visualTransformation = BirthdayTransformation(), - onClick = { - birthDayFocusRequester.requestFocus() - } - ) - - Spacer(modifier = Modifier.height(8.dp)) - when (state.birthdayValidationStatus) { - is BirthdayValidationStatus.Valid -> { - Spacer(modifier = Modifier.height(4.dp)) - } - - is BirthdayValidationStatus.Invalid -> { - NicknameValidMessageRow( - validMessage = R.string.birthday_error_invalid, - iconRes = R.drawable.ic_error, - validContentDescription = R.string.content_description_invalid_birthday, - color = AconTheme.color.Danger - ) - } - - else -> Spacer(modifier = Modifier.height(4.dp)) - } - } - - Spacer(Modifier.weight(1f)) - AconFilledButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 16.dp), - enabled = state.isEditButtonEnabled, - onClick = { onClickSaveButton() }, - content = { - Text( - text = stringResource(R.string.save), - style = AconTheme.typography.Title4, - fontWeight = FontWeight.SemiBold - ) - } - ) - } - } - } - } - } -} - -@Preview(showBackground = true) -@Composable -private fun ProfileModScreenPreview() { - AconTheme { - ProfileModScreenLegacy( - modifier = Modifier, - state = ProfileModStateLegacy.Success(), - navigateToBack = {}, - onNicknameChanged = {}, - onBirthdayChanged = {}, - onFocusChanged = { _, _ -> }, - onRequestExitDialog = {}, - onDisMissExitDialog = {}, - onRequestProfileEditModal = {}, - onDisMissProfileEditModal = {}, - onUpdateProfileImage = {}, - onClickSaveButton = {} - ) - } -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/FocusType.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/FocusType.kt deleted file mode 100644 index ffa803514..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/FocusType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.acon.acon.feature.profile.composable.type - -enum class FocusType { - Nickname, Birthday -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/NicknameErrorType.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/NicknameErrorType.kt deleted file mode 100644 index 35375e9b8..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/NicknameErrorType.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.acon.acon.feature.profile.composable.type - -import com.acon.acon.core.designsystem.R - -sealed class NicknameErrorType { - data object Invalid : NicknameErrorType() - data object Duplicate : NicknameErrorType() -} - -fun NicknameErrorType.validMessageResId(): Int = when (this) { - NicknameErrorType.Invalid -> R.string.nickname_error_invalid - NicknameErrorType.Duplicate -> R.string.nickname_error_duplicate -} - -fun NicknameErrorType.contentDescriptionResId(): Int = when (this) { - NicknameErrorType.Invalid-> R.string.content_description_empty_nickname - NicknameErrorType.Duplicate -> R.string.content_description_duplicate_nickname -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ProfileInfoType.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ProfileInfoType.kt deleted file mode 100644 index cd7eb11c9..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ProfileInfoType.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.acon.acon.feature.profile.composable.type - -enum class ProfileInfoType { - ACON, - AREA -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ProfileUpdateResult.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ProfileUpdateResult.kt deleted file mode 100644 index 36a1c9c7a..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ProfileUpdateResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.acon.acon.feature.profile.composable.type - -enum class ProfileUpdateResult { - SUCCESS, - FAILURE -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ValidationStatus.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ValidationStatus.kt deleted file mode 100644 index 4484ee783..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/type/ValidationStatus.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.acon.acon.feature.profile.composable.type - -sealed interface TextFieldStatus { - data object Empty : TextFieldStatus - data object Focused : TextFieldStatus - data object Inactive : TextFieldStatus - data object Error : TextFieldStatus -} - -sealed class NicknameValidationStatus { - data object Empty : NicknameValidationStatus() - data object Typing : NicknameValidationStatus() - data object Valid : NicknameValidationStatus() - data class Error(val errorTypes: NicknameErrorType) : NicknameValidationStatus() -} - -sealed class BirthdayValidationStatus { - data object Empty : BirthdayValidationStatus() - data object Invalid : BirthdayValidationStatus() - data object Valid : BirthdayValidationStatus() -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/utils/BirthdayTransformation.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/utils/BirthdayTransformation.kt deleted file mode 100644 index 8bd88dd1c..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/utils/BirthdayTransformation.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.acon.acon.feature.profile.composable.utils - -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 BirthdayTransformation : VisualTransformation { - override fun filter(text: AnnotatedString): TransformedText { - return birthdayFilter(text) - } -} - -fun birthdayFilter(text: AnnotatedString): TransformedText { - val trimmed = if (text.text.length >= 8) text.text.substring(0..7) else text.text - val builder = StringBuilder() - - for (i in trimmed.indices) { - builder.append(trimmed[i]) - if ((i == 3 || i == 5) && i < trimmed.length - 1) { - builder.append(".") - } - } - - val out = builder.toString() - - val numberOffsetTranslator = object : OffsetMapping { - override fun originalToTransformed(offset: Int): Int { - val clamped = offset.coerceIn(0, trimmed.length) - return when { - clamped <= 3 -> clamped - clamped <= 5 -> (clamped + 1).coerceAtMost(out.length) - clamped <= 7 -> (clamped + 2).coerceAtMost(out.length) - else -> out.length - } - } - - override fun transformedToOriginal(offset: Int): Int { - val clamped = offset.coerceIn(0, out.length) - return when { - clamped <= 4 -> clamped - clamped <= 7 -> clamped - 1 - clamped <= 10 -> clamped - 2 - else -> trimmed.length - } - } - } - - return TransformedText(AnnotatedString(out), numberOffsetTranslator) -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/utils/StringUtils.kt b/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/utils/StringUtils.kt deleted file mode 100644 index cddd1c742..000000000 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/utils/StringUtils.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.acon.acon.feature.profile.composable.utils - -fun Char.isAllowedChar(): Boolean { - return this in 'a'..'z' || - this in 'A'..'Z' || - this in '0'..'9' || - this == '.' || - this == '_' -} - -fun String.limitedNickname( - maxLength: Int = 14 -): Pair { - val limitedNickname = this.take(maxLength) - return limitedNickname to limitedNickname.length -} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkItem.kt b/feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkItem.kt new file mode 100644 index 000000000..b6d65b626 --- /dev/null +++ b/feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkItem.kt @@ -0,0 +1,113 @@ +package com.acon.feature.profile.savedspot.composable + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.acon.acon.core.designsystem.R +import com.acon.acon.core.designsystem.effect.imageGradientLayer +import com.acon.acon.core.designsystem.effect.imageGradientTopLayer +import com.acon.acon.core.designsystem.image.rememberDefaultLoadImageErrorPainter +import com.acon.acon.core.designsystem.theme.AconTheme +import com.acon.acon.core.model.model.profile.SavedSpot +import com.acon.acon.core.model.model.profile.SpotThumbnailStatus + +@Composable +internal fun BookmarkItem( + spot: SavedSpot, + onClickSpotItem:() -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(8.dp)) + .clickable { onClickSpotItem() } + ) { + when(val thumbnailStatus = spot.spotThumbnail) { + is SpotThumbnailStatus.Exist -> { + AsyncImage( + model = thumbnailStatus.url, + contentDescription = stringResource(R.string.store_background_image_content_description), + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .imageGradientTopLayer(), + error = rememberDefaultLoadImageErrorPainter() + ) + + Text( + text = spot.spotName.let { name -> + if (name.length > 9) name.take(8) + "…" else name + }, + color = AconTheme.color.White, + style = AconTheme.typography.Title5, + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth() + .padding(top = 20.dp) + .padding(horizontal = 20.dp) + ) + } + is SpotThumbnailStatus.Empty -> { + + Image( + painter = painterResource(R.drawable.ic_bg_no_store_profile), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .imageGradientLayer() + ) + + Text( + text = spot.spotName.let { name -> + if (name.length > 9) name.take(8) + "…" else name + }, + color = AconTheme.color.White, + style = AconTheme.typography.Title5, + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.TopCenter) + .padding(top = 20.dp) + .padding(horizontal = 20.dp) + ) + + Text( + text = stringResource(R.string.no_store_image), + color = AconTheme.color.Gray50, + style = AconTheme.typography.Caption1, + modifier = Modifier + .align(Alignment.Center) + ) + } + } + } +} + +@Preview +@Composable +private fun BookmarkItemPreview() { + AconTheme { + BookmarkItem( + spot = SavedSpot(0, "샘플", SpotThumbnailStatus.Empty), + onClickSpotItem = {} + ) + } +} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/composable/BookmarkScreen.kt b/feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreen.kt similarity index 92% rename from feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/composable/BookmarkScreen.kt rename to feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreen.kt index 808e77740..a472e43de 100644 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/composable/BookmarkScreen.kt +++ b/feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreen.kt @@ -1,4 +1,4 @@ -package com.acon.acon.feature.profile.composable.screen.bookmark.composable +package com.acon.feature.profile.savedspot.composable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -34,12 +34,9 @@ import com.acon.acon.core.designsystem.component.topbar.AconTopBar import com.acon.acon.core.designsystem.effect.LocalHazeState import com.acon.acon.core.designsystem.effect.defaultHazeEffect import com.acon.acon.core.designsystem.theme.AconTheme -import com.acon.acon.feature.profile.composable.screen.bookmark.BookmarkUiState -import com.acon.acon.feature.profile.composable.screen.mockSpotList -import com.acon.acon.feature.profile.composable.screen.profile.composable.BookmarkItemLegacy -import com.acon.acon.feature.profile.composable.screen.profile.composable.BookmarkSkeletonItemLegacy import com.acon.acon.core.ui.compose.LocalOnRetry import com.acon.acon.core.ui.compose.getScreenHeight +import com.acon.feature.profile.savedspot.viewmodel.BookmarkUiState import dev.chrisbanes.haze.hazeSource @Composable @@ -97,7 +94,7 @@ fun BookmarkScreen( .verticalScroll(rememberScrollState()) .hazeSource(LocalHazeState.current) ) { - mockSpotList.chunked(2).forEach { rowItems -> + (1..9).chunked(2).forEach { rowItems -> Row( modifier = Modifier .fillMaxWidth() @@ -105,7 +102,7 @@ fun BookmarkScreen( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { rowItems.forEach { spot -> - BookmarkSkeletonItemLegacy( + BookmarkSkeletonItem( skeletonHeight = skeletonHeight, modifier = Modifier .weight(1f) @@ -166,7 +163,7 @@ fun BookmarkScreen( .verticalScroll(rememberScrollState()) .hazeSource(LocalHazeState.current) ) { - state.savedSpotLegacies?.chunked(2)?.fastForEach { rowItems -> + state.savedSpots.chunked(2).fastForEach { rowItems -> Row( modifier = Modifier .fillMaxWidth() @@ -174,7 +171,7 @@ fun BookmarkScreen( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { rowItems.forEach { spot -> - BookmarkItemLegacy( + BookmarkItem( spot = spot, onClickSpotItem = { onSpotClick(spot.spotId) }, modifier = Modifier diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/composable/BookmarkScreenContainer.kt b/feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreenContainer.kt similarity index 80% rename from feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/composable/BookmarkScreenContainer.kt rename to feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreenContainer.kt index 057c7ac73..185c41864 100644 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/composable/BookmarkScreenContainer.kt +++ b/feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkScreenContainer.kt @@ -1,11 +1,11 @@ -package com.acon.acon.feature.profile.composable.screen.bookmark.composable +package com.acon.feature.profile.savedspot.composable import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel -import com.acon.acon.feature.profile.composable.screen.bookmark.BookmarkUiSideEffect -import com.acon.acon.feature.profile.composable.screen.bookmark.BookmarkViewModel +import com.acon.feature.profile.savedspot.viewmodel.BookmarkUiSideEffect +import com.acon.feature.profile.savedspot.viewmodel.BookmarkViewModel import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkSkeletonItemLegacy.kt b/feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkSkeletonItem.kt similarity index 90% rename from feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkSkeletonItemLegacy.kt rename to feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkSkeletonItem.kt index 5035811ac..4bea33af9 100644 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/profile/composable/BookmarkSkeletonItemLegacy.kt +++ b/feature/profile/src/main/java/com/acon/feature/profile/savedspot/composable/BookmarkSkeletonItem.kt @@ -1,4 +1,4 @@ -package com.acon.acon.feature.profile.composable.screen.profile.composable +package com.acon.feature.profile.savedspot.composable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -13,7 +13,7 @@ import com.acon.acon.core.designsystem.animation.skeleton import com.acon.acon.core.designsystem.component.loading.SkeletonItem @Composable -fun BookmarkSkeletonItemLegacy( +fun BookmarkSkeletonItem( skeletonHeight: Dp, modifier: Modifier = Modifier ) { diff --git a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/BookmarkViewModel.kt b/feature/profile/src/main/java/com/acon/feature/profile/savedspot/viewmodel/BookmarkViewModel.kt similarity index 59% rename from feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/BookmarkViewModel.kt rename to feature/profile/src/main/java/com/acon/feature/profile/savedspot/viewmodel/BookmarkViewModel.kt index a1ebdf5e8..84ae2167f 100644 --- a/feature/profile/src/main/java/com/acon/acon/feature/profile/composable/screen/bookmark/BookmarkViewModel.kt +++ b/feature/profile/src/main/java/com/acon/feature/profile/savedspot/viewmodel/BookmarkViewModel.kt @@ -1,7 +1,9 @@ -package com.acon.acon.feature.profile.composable.screen.bookmark +package com.acon.feature.profile.savedspot.viewmodel -import com.acon.acon.domain.repository.SpotRepository +import androidx.compose.runtime.Immutable +import com.acon.acon.core.model.model.profile.SavedSpot import com.acon.acon.core.ui.base.BaseContainerHost +import com.acon.acon.domain.repository.ProfileRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import org.orbitmvi.orbit.annotation.OrbitExperimental @@ -11,27 +13,24 @@ import javax.inject.Inject @OptIn(OrbitExperimental::class) @HiltViewModel class BookmarkViewModel @Inject constructor( - private val spotRepository: SpotRepository + private val profileRepository: ProfileRepository ) : BaseContainerHost() { override val container = container(BookmarkUiState.Loading) { - fetchSavedSpotList() - } - - private fun fetchSavedSpotList() = intent { - delay(800) - spotRepository.fetchSavedSpotList().onSuccess { - reduce { - BookmarkUiState.Success(savedSpotLegacies = it) - } - }.onFailure { - reduce { - BookmarkUiState.LoadFailed + delay(LOADING_DELAY_MILLIS) + profileRepository.getSavedSpots().collect { result -> + result.onSuccess { + reduce { + BookmarkUiState.Success(savedSpots = it) + } + }.onFailure { + reduce { + BookmarkUiState.LoadFailed + } } } } - fun navigateToBack() = intent { postSideEffect(BookmarkUiSideEffect.OnNavigateToBack) } @@ -41,10 +40,17 @@ class BookmarkViewModel @Inject constructor( postSideEffect(BookmarkUiSideEffect.OnNavigateToSpotDetailScreen(spotId)) } } + + companion object { + private const val LOADING_DELAY_MILLIS = 800L + } } sealed interface BookmarkUiState { - data class Success(val savedSpotLegacies: List? = emptyList()) : BookmarkUiState + @Immutable + data class Success( + val savedSpots: List + ) : BookmarkUiState data object Loading : BookmarkUiState data object LoadFailed : BookmarkUiState } diff --git a/feature/settings/src/main/java/com/acon/acon/feature/verification/screen/UserVerifiedAreasViewModel.kt b/feature/settings/src/main/java/com/acon/acon/feature/verification/screen/UserVerifiedAreasViewModel.kt index 39c6546f9..1822bf0e8 100644 --- a/feature/settings/src/main/java/com/acon/acon/feature/verification/screen/UserVerifiedAreasViewModel.kt +++ b/feature/settings/src/main/java/com/acon/acon/feature/verification/screen/UserVerifiedAreasViewModel.kt @@ -2,8 +2,9 @@ package com.acon.acon.feature.verification.screen import com.acon.acon.core.ui.base.BaseContainerHost import com.acon.acon.domain.error.area.DeleteVerifiedAreaError -import com.acon.acon.domain.repository.ProfileRepositoryLegacy +import com.acon.acon.domain.repository.ProfileRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.annotation.OrbitExperimental import org.orbitmvi.orbit.viewmodel.container @@ -13,29 +14,34 @@ import javax.inject.Inject @OptIn(OrbitExperimental::class) @HiltViewModel class UserVerifiedAreasViewModel @Inject constructor( - private val profileRepositoryLegacy: ProfileRepositoryLegacy + private val profileRepository: ProfileRepository ) : BaseContainerHost() { + private var loadVerifiedAreasJob: Job? = null + override val container: Container = container(UserVerifiedAreasUiState.Loading) { - fetchVerifiedAreaList() + loadVerifiedAreasJob = loadVerifiedAreas() } - private fun fetchVerifiedAreaList() = intent { - profileRepositoryLegacy.fetchVerifiedAreaList() - .onSuccess { + private fun loadVerifiedAreas() = intent { + profileRepository.getVerifiedAreas().collect { result -> + result.onSuccess { areas -> reduce { - UserVerifiedAreasUiState.Success(verificationAreaList = it) + UserVerifiedAreasUiState.Success(verificationAreaList = areas) + } + }.onFailure { + reduce { + UserVerifiedAreasUiState.LoadFailed } } - .onFailure { - UserVerifiedAreasUiState.LoadFailed - } + } } fun retry() = intent { reduce { UserVerifiedAreasUiState.Loading } - fetchVerifiedAreaList() + loadVerifiedAreasJob?.cancel() + loadVerifiedAreas() } private fun showAreaDeleteFailDialog() = intent { @@ -71,10 +77,7 @@ class UserVerifiedAreasViewModel @Inject constructor( } fun deleteVerifiedArea(verifiedAreaId: Long) = intent { - profileRepositoryLegacy.deleteVerifiedArea(verifiedAreaId) - .onSuccess { - fetchVerifiedAreaList() - } + profileRepository.deleteVerifiedArea(verifiedAreaId) .onFailure { error -> when (error) { is DeleteVerifiedAreaError.InvalidVerifiedArea -> { diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt index 5c8bd1d19..f6e8f1137 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailViewModel.kt @@ -7,14 +7,16 @@ import androidx.navigation.toRoute import com.acon.acon.core.analytics.amplitude.AconAmplitude import com.acon.acon.core.analytics.constants.EventNames import com.acon.acon.core.analytics.constants.PropertyKeys +import com.acon.acon.core.model.type.SignInStatus import com.acon.acon.core.navigation.route.SpotRoute import com.acon.acon.core.navigation.type.spotNavigationParameterNavType import com.acon.acon.core.ui.base.BaseContainerHost -import com.acon.acon.domain.repository.ProfileRepositoryLegacy +import com.acon.acon.domain.repository.ProfileRepository import com.acon.acon.domain.repository.SpotRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.firstOrNull import org.orbitmvi.orbit.annotation.OrbitExperimental import org.orbitmvi.orbit.viewmodel.container import javax.annotation.concurrent.Immutable @@ -24,7 +26,7 @@ import javax.inject.Inject @HiltViewModel class SpotDetailViewModel @Inject constructor( private val spotRepository: SpotRepository, - private val profileRepositoryLegacy: ProfileRepositoryLegacy, + private val profileRepository: ProfileRepository, savedStateHandle: SavedStateHandle ) : BaseContainerHost() { @@ -45,7 +47,7 @@ class SpotDetailViewModel @Inject constructor( container(SpotDetailUiState.Loading) { signInStatus.collect { when (it) { - com.acon.acon.core.model.type.SignInStatus.GUEST -> { + SignInStatus.GUEST -> { if (spotNavData.isFromDeepLink == true) { fetchedSpotDetail() } else { @@ -82,8 +84,8 @@ class SpotDetailViewModel @Inject constructor( // GUEST 인 경우 빈 리스트 val verifiedAreaListDeferred = viewModelScope.async { - if (signInStatus.value != com.acon.acon.core.model.type.SignInStatus.GUEST) { - profileRepositoryLegacy.fetchVerifiedAreaList() + if (signInStatus.value != SignInStatus.GUEST) { + profileRepository.getVerifiedAreas().firstOrNull() } else { Result.success(emptyList()) } @@ -94,7 +96,7 @@ class SpotDetailViewModel @Inject constructor( reduce { val isAreaVerified = verifiedAreaListResult - .getOrNull() + ?.getOrNull() .orEmpty() .isNotEmpty()