diff --git a/.github/workflows/debug_build_ci.yml b/.github/workflows/debug_build_ci.yml index 261ac31ab..89506ee3f 100644 --- a/.github/workflows/debug_build_ci.yml +++ b/.github/workflows/debug_build_ci.yml @@ -25,7 +25,7 @@ jobs: echo '${{ secrets.LOCAL_PROPERTIES }}' >> ./local.properties - name: Access Google-Service file - run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./app/google-services.json + run: echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > ./acon/google-services.json - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/app/.gitignore b/acon/.gitignore similarity index 100% rename from app/.gitignore rename to acon/.gitignore diff --git a/app/build.gradle.kts b/acon/build.gradle.kts similarity index 76% rename from app/build.gradle.kts rename to acon/build.gradle.kts index 2514033ef..2732be763 100644 --- a/app/build.gradle.kts +++ b/acon/build.gradle.kts @@ -1,5 +1,3 @@ -import utils.androidTestImplementation - /** See AndroidApplicationConventionPlugin.kt */ plugins { @@ -9,11 +7,24 @@ plugins { alias(libs.plugins.acon.android.library.haze) alias(libs.plugins.acon.android.library.naver.map) alias(libs.plugins.acon.firebase) + alias(libs.plugins.acon.common.unit.test) } android { namespace = "com.acon.acon" + flavorDimensions += "distributionChannel" + productFlavors { + create("qa") { + dimension = "distributionChannel" + buildConfigField("boolean", "IS_QA_BUILD", "true") + } + create("production") { + dimension = "distributionChannel" + buildConfigField("boolean", "IS_QA_BUILD", "false") + } + } + buildTypes { debug { applicationIdSuffix = ".debug" @@ -35,11 +46,19 @@ android { } } +androidComponents { + beforeVariants(selector().all()) { variant -> + if (variant.name == "productionDebug") { + variant.enable = false + } + } +} + dependencies { implementation(projects.core.designsystem) implementation(projects.core.map) - implementation(projects.core.adsApi) + implementation(projects.core.ads) implementation(projects.core.analytics) implementation(projects.core.navigation) implementation(projects.core.ui) @@ -56,20 +75,10 @@ dependencies { implementation(projects.feature.settings) implementation(projects.feature.profile) - implementation(projects.provider.adsImpl) - implementation(libs.branch.io) implementation(libs.google.services.ads) implementation(libs.play.services.location) implementation(libs.startup) implementation(libs.androidx.core.splashscreen) - - testImplementation(libs.bundles.non.android.test) - testRuntimeOnly(libs.bundles.junit5.runtime) - androidTestImplementation(libs.bundles.android.test) -} - -tasks.withType { - useJUnitPlatform() } \ No newline at end of file diff --git a/app/keystore/keystore_base64.txt b/acon/keystore/keystore_base64.txt similarity index 100% rename from app/keystore/keystore_base64.txt rename to acon/keystore/keystore_base64.txt diff --git a/app/proguard-rules.pro b/acon/proguard-rules.pro similarity index 97% rename from app/proguard-rules.pro rename to acon/proguard-rules.pro index 1abedafc5..652c73392 100644 --- a/app/proguard-rules.pro +++ b/acon/proguard-rules.pro @@ -138,4 +138,6 @@ -dontwarn androidx.** -keep class androidx.** { *; } --keep interface androidx.** { *; } \ No newline at end of file +-keep interface androidx.** { *; } + +-keep class com.acon.core.data.dto.entity.OnboardingPreferencesEntity { *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/acon/src/main/AndroidManifest.xml similarity index 100% rename from app/src/main/AndroidManifest.xml rename to acon/src/main/AndroidManifest.xml diff --git a/app/src/main/ic_launcher-playstore.png b/acon/src/main/ic_launcher-playstore.png similarity index 100% rename from app/src/main/ic_launcher-playstore.png rename to acon/src/main/ic_launcher-playstore.png diff --git a/app/src/main/java/com/acon/acon/AconApplication.kt b/acon/src/main/java/com/acon/acon/AconApplication.kt similarity index 100% rename from app/src/main/java/com/acon/acon/AconApplication.kt rename to acon/src/main/java/com/acon/acon/AconApplication.kt diff --git a/app/src/main/java/com/acon/acon/MainActivity.kt b/acon/src/main/java/com/acon/acon/MainActivity.kt similarity index 91% rename from app/src/main/java/com/acon/acon/MainActivity.kt rename to acon/src/main/java/com/acon/acon/MainActivity.kt index 05221be7e..1ea474b0e 100644 --- a/app/src/main/java/com/acon/acon/MainActivity.kt +++ b/acon/src/main/java/com/acon/acon/MainActivity.kt @@ -35,8 +35,6 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.rememberNavController -import com.acon.acon.core.ads_api.AdProvider -import com.acon.acon.core.ads_api.LocalSpotListAdProvider import com.acon.acon.core.analytics.amplitude.AconAmplitude import com.acon.acon.core.analytics.constants.EventNames import com.acon.acon.core.common.DeepLinkHandler @@ -50,6 +48,7 @@ import com.acon.acon.core.designsystem.effect.rememberHazeState import com.acon.acon.core.designsystem.theme.AconTheme import com.acon.acon.core.navigation.LocalNavController import com.acon.acon.core.navigation.route.AreaVerificationRoute +import com.acon.acon.core.navigation.route.OnboardingRoute import com.acon.acon.core.navigation.route.SpotRoute import com.acon.acon.core.navigation.utils.navigateAndClear import com.acon.acon.core.ui.activityComponentEntryPoint @@ -58,16 +57,15 @@ import com.acon.acon.core.ui.compose.LocalDeepLinkHandler import com.acon.acon.core.ui.compose.LocalLocation import com.acon.acon.core.ui.compose.LocalRequestLocationPermission import com.acon.acon.core.ui.compose.LocalRequestSignIn -import com.acon.acon.core.ui.compose.LocalSnackbarHostState import com.acon.acon.core.ui.compose.LocalSignInStatus +import com.acon.acon.core.ui.compose.LocalSnackbarHostState import com.acon.acon.domain.repository.AconAppRepository +import com.acon.acon.domain.repository.OnboardingRepository import com.acon.acon.domain.repository.UserRepository import com.acon.acon.navigation.AconNavigation -import com.acon.acon.provider.ads_impl.SpotListAdProvider import com.acon.acon.update.AppUpdateHandler import com.acon.acon.update.AppUpdateHandlerImpl import com.acon.acon.update.UpdateState -import com.acon.core.social.client.GoogleAuthClient import com.acon.core.social.di.AuthClientEntryPoint import com.google.android.gms.common.api.ResolvableApiException import com.google.android.gms.location.LocationCallback @@ -105,6 +103,9 @@ class MainActivity : ComponentActivity() { @Inject lateinit var userRepository: UserRepository + @Inject + lateinit var onboardingRepository: OnboardingRepository + @Inject lateinit var aconAppRepository: AconAppRepository @@ -112,7 +113,6 @@ class MainActivity : ComponentActivity() { private val deepLinkHandler = DeepLinkHandler() - private val spotListAdProvider: AdProvider = SpotListAdProvider() private val gpsResolutionResultLauncher = registerForActivityResult( ActivityResultContracts.StartIntentSenderForResult() ) { result -> @@ -124,8 +124,11 @@ class MainActivity : ComponentActivity() { } private val appUpdateManager by lazy { - AppUpdateManagerFactory.create(application) + if (!BuildConfig.IS_QA_BUILD) + AppUpdateManagerFactory.create(application) + else null } + private val appUpdateActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result -> when (result.resultCode) { @@ -152,14 +155,14 @@ class MainActivity : ComponentActivity() { ) when (result) { SnackbarResult.ActionPerformed -> { - appUpdateManager.completeUpdate() + appUpdateManager?.completeUpdate() } SnackbarResult.Dismissed -> Unit } } } else if (state.installStatus() == InstallStatus.INSTALLED) { - appUpdateManager.unregisterListener(this) + appUpdateManager?.unregisterListener(this) } } } @@ -167,7 +170,7 @@ class MainActivity : ComponentActivity() { private val appUpdateHandler: AppUpdateHandler by lazy { AppUpdateHandlerImpl( - appUpdateManager = appUpdateManager.apply { + appUpdateManager = appUpdateManager?.apply { registerListener(appInstallStateListener) }, aconAppRepository = aconAppRepository, @@ -328,7 +331,6 @@ class MainActivity : ComponentActivity() { viewModel.updateAmplPropertyKey(it) }, LocalRequestLocationPermission provides ::requestLocationPermission, - LocalSpotListAdProvider provides spotListAdProvider, LocalDeepLinkHandler provides deepLinkHandler ) { AconNavigation( @@ -346,25 +348,28 @@ class MainActivity : ComponentActivity() { val code = client.getCredentialCode() ?: return@launch userRepository.signIn(client.platform, code) - .onSuccess { verificationStatus -> - if (verificationStatus.hasVerifiedArea) { - navController.navigate(SpotRoute.SpotList) { - popUpTo(navController.graph.id) { - inclusive = true - } - } - } else { - navController.navigateAndClear( - AreaVerificationRoute.AreaVerification - ) - } + .onSuccess { externalUUID -> if (appState.propertyKey.isNotBlank()) { AconAmplitude.trackEvent( eventName = EventNames.GUEST, property = appState.propertyKey to true ) } - AconAmplitude.setUserId(verificationStatus.externalUUID) + AconAmplitude.setUserId(externalUUID.value) + + onboardingRepository.getOnboardingPreferences().onSuccess { pref -> + if (pref.shouldShowIntroduce) { + navController.navigateAndClear(OnboardingRoute.Introduce) + } else if (pref.shouldVerifyArea) { + navController.navigateAndClear(AreaVerificationRoute.AreaVerification) + } else if (pref.shouldChooseDislikes) { + navController.navigateAndClear(OnboardingRoute.ChooseDislikes) + } else { + navController.navigateAndClear(SpotRoute.SpotList) + } + }.onFailure { + navController.navigateAndClear(SpotRoute.SpotList) + } }.onFailure { e -> Timber.e(e) } @@ -478,9 +483,9 @@ class MainActivity : ComponentActivity() { } DisposableEffect(appUpdateManager) { - appUpdateManager.registerListener(appInstallStateListener) + appUpdateManager?.registerListener(appInstallStateListener) onDispose { - appUpdateManager.unregisterListener(appInstallStateListener) + appUpdateManager?.unregisterListener(appInstallStateListener) } } } diff --git a/app/src/main/java/com/acon/acon/MainViewModel.kt b/acon/src/main/java/com/acon/acon/MainViewModel.kt similarity index 100% rename from app/src/main/java/com/acon/acon/MainViewModel.kt rename to acon/src/main/java/com/acon/acon/MainViewModel.kt diff --git a/app/src/main/java/com/acon/acon/di/AuthenticatorModule.kt b/acon/src/main/java/com/acon/acon/di/AuthenticatorModule.kt similarity index 100% rename from app/src/main/java/com/acon/acon/di/AuthenticatorModule.kt rename to acon/src/main/java/com/acon/acon/di/AuthenticatorModule.kt diff --git a/app/src/main/java/com/acon/acon/di/CoroutineDispatchersModule.kt b/acon/src/main/java/com/acon/acon/di/CoroutineDispatchersModule.kt similarity index 100% rename from app/src/main/java/com/acon/acon/di/CoroutineDispatchersModule.kt rename to acon/src/main/java/com/acon/acon/di/CoroutineDispatchersModule.kt diff --git a/app/src/main/java/com/acon/acon/di/CoroutineScopesModule.kt b/acon/src/main/java/com/acon/acon/di/CoroutineScopesModule.kt similarity index 100% rename from app/src/main/java/com/acon/acon/di/CoroutineScopesModule.kt rename to acon/src/main/java/com/acon/acon/di/CoroutineScopesModule.kt diff --git a/app/src/main/java/com/acon/acon/launcher/AppLauncherImpl.kt b/acon/src/main/java/com/acon/acon/launcher/AppLauncherImpl.kt similarity index 100% rename from app/src/main/java/com/acon/acon/launcher/AppLauncherImpl.kt rename to acon/src/main/java/com/acon/acon/launcher/AppLauncherImpl.kt diff --git a/app/src/main/java/com/acon/acon/navigation/AconNavigation.kt b/acon/src/main/java/com/acon/acon/navigation/AconNavigation.kt similarity index 100% rename from app/src/main/java/com/acon/acon/navigation/AconNavigation.kt rename to acon/src/main/java/com/acon/acon/navigation/AconNavigation.kt diff --git a/app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt b/acon/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt similarity index 97% rename from app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt rename to acon/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt index ec71bea3f..17361b5e0 100644 --- a/app/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt +++ b/acon/src/main/java/com/acon/acon/navigation/nested/AreaVerificationNavigation.kt @@ -35,7 +35,8 @@ fun NavGraphBuilder.areaVerificationNavigation( skippable = LocalNavController.current.hasPreviousBackStackEntry().not(), onNavigateToChooseDislikes = { navController.navigateAndClear(OnboardingRoute.ChooseDislikes) }, onNavigateToIntroduce = { navController.navigateAndClear(OnboardingRoute.Introduce) }, - onNavigateToSpotList = { navController.navigateAndClear(SpotRoute.SpotList) } + onNavigateToSpotList = { navController.navigateAndClear(SpotRoute.SpotList) }, + onNavigateBack = navController::navigateUp ) } diff --git a/app/src/main/java/com/acon/acon/navigation/nested/OnboardingNavigation.kt b/acon/src/main/java/com/acon/acon/navigation/nested/OnboardingNavigation.kt similarity index 86% rename from app/src/main/java/com/acon/acon/navigation/nested/OnboardingNavigation.kt rename to acon/src/main/java/com/acon/acon/navigation/nested/OnboardingNavigation.kt index 2d20589b7..f32d25578 100644 --- a/app/src/main/java/com/acon/acon/navigation/nested/OnboardingNavigation.kt +++ b/acon/src/main/java/com/acon/acon/navigation/nested/OnboardingNavigation.kt @@ -10,6 +10,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import com.acon.acon.core.designsystem.effect.screenDefault import com.acon.acon.core.navigation.LocalNavController +import com.acon.acon.core.navigation.route.AreaVerificationRoute import com.acon.acon.core.navigation.route.OnboardingRoute import com.acon.acon.core.navigation.route.SettingsRoute import com.acon.acon.core.navigation.route.SpotRoute @@ -51,6 +52,12 @@ internal fun NavGraphBuilder.onboardingNavigation( modifier = Modifier.screenDefault().statusBarsPadding(), onNavigateToHome = { navController.navigateAndClear(SpotRoute.Graph) + }, + onNavigateToAreaVerification = { + navController.navigateAndClear(AreaVerificationRoute.Graph) + }, + onNavigateToChooseDislikes = { + navController.navigateAndClear(OnboardingRoute.ChooseDislikes) } ) } diff --git a/app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt b/acon/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt similarity index 100% rename from app/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt rename to acon/src/main/java/com/acon/acon/navigation/nested/ProfileNavigation.kt diff --git a/app/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt b/acon/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt similarity index 100% rename from app/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt rename to acon/src/main/java/com/acon/acon/navigation/nested/SettingsNavigation.kt diff --git a/app/src/main/java/com/acon/acon/navigation/nested/SignInNavigation.kt b/acon/src/main/java/com/acon/acon/navigation/nested/SignInNavigation.kt similarity index 100% rename from app/src/main/java/com/acon/acon/navigation/nested/SignInNavigation.kt rename to acon/src/main/java/com/acon/acon/navigation/nested/SignInNavigation.kt diff --git a/app/src/main/java/com/acon/acon/navigation/nested/SpotNavigation.kt b/acon/src/main/java/com/acon/acon/navigation/nested/SpotNavigation.kt similarity index 96% rename from app/src/main/java/com/acon/acon/navigation/nested/SpotNavigation.kt rename to acon/src/main/java/com/acon/acon/navigation/nested/SpotNavigation.kt index 497281532..f9e920e4c 100644 --- a/app/src/main/java/com/acon/acon/navigation/nested/SpotNavigation.kt +++ b/acon/src/main/java/com/acon/acon/navigation/nested/SpotNavigation.kt @@ -57,6 +57,9 @@ internal fun NavGraphBuilder.spotNavigation( onNavigateToAreaVerificationScreen = { lat, lon -> navController.navigate(AreaVerificationRoute.VerifyInMap) }, + onNavigateToUploadPlace = { + navController.navigate(UploadRoute.Place) + }, modifier = Modifier .fillMaxSize() .background(AconTheme.color.Gray900) diff --git a/app/src/main/java/com/acon/acon/navigation/nested/UploadNavigation.kt b/acon/src/main/java/com/acon/acon/navigation/nested/UploadNavigation.kt similarity index 100% rename from app/src/main/java/com/acon/acon/navigation/nested/UploadNavigation.kt rename to acon/src/main/java/com/acon/acon/navigation/nested/UploadNavigation.kt diff --git a/app/src/main/java/com/acon/acon/startup/AconAmplitudeInitializer.kt b/acon/src/main/java/com/acon/acon/startup/AconAmplitudeInitializer.kt similarity index 100% rename from app/src/main/java/com/acon/acon/startup/AconAmplitudeInitializer.kt rename to acon/src/main/java/com/acon/acon/startup/AconAmplitudeInitializer.kt diff --git a/app/src/main/java/com/acon/acon/startup/BranchInitializer.kt b/acon/src/main/java/com/acon/acon/startup/BranchInitializer.kt similarity index 100% rename from app/src/main/java/com/acon/acon/startup/BranchInitializer.kt rename to acon/src/main/java/com/acon/acon/startup/BranchInitializer.kt diff --git a/app/src/main/java/com/acon/acon/startup/MobileAdsInitializer.kt b/acon/src/main/java/com/acon/acon/startup/MobileAdsInitializer.kt similarity index 100% rename from app/src/main/java/com/acon/acon/startup/MobileAdsInitializer.kt rename to acon/src/main/java/com/acon/acon/startup/MobileAdsInitializer.kt diff --git a/app/src/main/java/com/acon/acon/startup/TimberInitializer.kt b/acon/src/main/java/com/acon/acon/startup/TimberInitializer.kt similarity index 100% rename from app/src/main/java/com/acon/acon/startup/TimberInitializer.kt rename to acon/src/main/java/com/acon/acon/startup/TimberInitializer.kt diff --git a/app/src/main/java/com/acon/acon/update/AppUpdateHandler.kt b/acon/src/main/java/com/acon/acon/update/AppUpdateHandler.kt similarity index 93% rename from app/src/main/java/com/acon/acon/update/AppUpdateHandler.kt rename to acon/src/main/java/com/acon/acon/update/AppUpdateHandler.kt index 6bff5f2d4..29ee94d26 100644 --- a/app/src/main/java/com/acon/acon/update/AppUpdateHandler.kt +++ b/acon/src/main/java/com/acon/acon/update/AppUpdateHandler.kt @@ -35,7 +35,7 @@ interface AppUpdateHandler { } class AppUpdateHandlerImpl( - private val appUpdateManager: AppUpdateManager, + private val appUpdateManager: AppUpdateManager?, private val aconAppRepository: AconAppRepository, private val appUpdateActivityResultLauncher: ActivityResultLauncher, private val application: Application, @@ -43,7 +43,9 @@ class AppUpdateHandlerImpl( ) : AppUpdateHandler { private val appUpdateInfo = flow { - emit(appUpdateManager.appUpdateInfo.await()) + appUpdateManager?.appUpdateInfo?.let { + emit(it.await()) + } }.stateIn( scope = scope, started = SharingStarted.WhileSubscribed(5_000), @@ -56,7 +58,7 @@ class AppUpdateHandlerImpl( val packageInfo = application.packageManager.getPackageInfo(application.packageName, 0) packageInfo.versionName - } catch (e: Exception) { + } catch (_: Exception) { null } if (currentAppVersion == null) @@ -80,7 +82,7 @@ class AppUpdateHandlerImpl( } override fun startFlexibleUpdate() { - appUpdateManager.startUpdateFlowForResult( + appUpdateManager?.startUpdateFlowForResult( appUpdateInfo.value ?: return, appUpdateActivityResultLauncher, AppUpdateOptions.defaultOptions(AppUpdateType.FLEXIBLE) diff --git a/app/src/main/res/drawable/aconlogo.xml b/acon/src/main/res/drawable/aconlogo.xml similarity index 100% rename from app/src/main/res/drawable/aconlogo.xml rename to acon/src/main/res/drawable/aconlogo.xml diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/acon/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_background.xml rename to acon/src/main/res/drawable/ic_launcher_background.xml diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/acon/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_foreground.xml rename to acon/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/app/src/main/res/drawable/launch_background.xml b/acon/src/main/res/drawable/launch_background.xml similarity index 100% rename from app/src/main/res/drawable/launch_background.xml rename to acon/src/main/res/drawable/launch_background.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/acon/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to acon/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/acon/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to acon/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/acon/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher.webp rename to acon/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/acon/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp rename to acon/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/acon/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to acon/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/acon/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher.webp rename to acon/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/acon/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp rename to acon/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/acon/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to acon/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/acon/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to acon/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/acon/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp rename to acon/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/acon/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to acon/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/acon/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to acon/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/acon/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp rename to acon/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/acon/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to acon/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/acon/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to acon/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/acon/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp rename to acon/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/acon/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to acon/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/app/src/main/res/values-v31/themes.xml b/acon/src/main/res/values-v31/themes.xml similarity index 100% rename from app/src/main/res/values-v31/themes.xml rename to acon/src/main/res/values-v31/themes.xml diff --git a/app/src/main/res/values/colors.xml b/acon/src/main/res/values/colors.xml similarity index 100% rename from app/src/main/res/values/colors.xml rename to acon/src/main/res/values/colors.xml diff --git a/app/src/main/res/values/ic_launcher_background.xml b/acon/src/main/res/values/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/values/ic_launcher_background.xml rename to acon/src/main/res/values/ic_launcher_background.xml diff --git a/app/src/main/res/values/strings.xml b/acon/src/main/res/values/strings.xml similarity index 100% rename from app/src/main/res/values/strings.xml rename to acon/src/main/res/values/strings.xml diff --git a/app/src/main/res/values/themes.xml b/acon/src/main/res/values/themes.xml similarity index 100% rename from app/src/main/res/values/themes.xml rename to acon/src/main/res/values/themes.xml diff --git a/app/src/main/res/xml/backup_rules.xml b/acon/src/main/res/xml/backup_rules.xml similarity index 100% rename from app/src/main/res/xml/backup_rules.xml rename to acon/src/main/res/xml/backup_rules.xml diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/acon/src/main/res/xml/data_extraction_rules.xml similarity index 100% rename from app/src/main/res/xml/data_extraction_rules.xml rename to acon/src/main/res/xml/data_extraction_rules.xml diff --git a/app/src/test/kotlin/com/acon/acon/update/AppUpdateHandlerImplTest.kt b/acon/src/test/kotlin/com/acon/acon/update/AppUpdateHandlerImplTest.kt similarity index 100% rename from app/src/test/kotlin/com/acon/acon/update/AppUpdateHandlerImplTest.kt rename to acon/src/test/kotlin/com/acon/acon/update/AppUpdateHandlerImplTest.kt diff --git a/app/src/androidTest/java/com/acon/acon/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/acon/acon/ExampleInstrumentedTest.kt deleted file mode 100644 index dfa50cba8..000000000 --- a/app/src/androidTest/java/com/acon/acon/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.acon.acon - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.android.acon", appContext.packageName) - } -} \ No newline at end of file diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 8eaa808fb..6ad99feda 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -61,5 +61,13 @@ gradlePlugin { id = "com.acon.firebase" implementationClass = "FirebaseConventionPlugin" } + register("featureTest") { + id = "com.acon.feature.test" + implementationClass = "test.FeatureTestConventionPlugin" + } + register("commonUnitTest") { + id = "com.acon.common.unit.test" + implementationClass = "test.CommonUnitTestConventionPlugin" + } } } \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt index f0dd0ec47..b8d8970c4 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -20,6 +20,7 @@ class AndroidApplicationComposeConventionPlugin: Plugin { extensions.configure { buildFeatures { compose = true + buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = diff --git a/build-logic/convention/src/main/kotlin/test/CommonUnitTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/test/CommonUnitTestConventionPlugin.kt new file mode 100644 index 000000000..b57d5d597 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/test/CommonUnitTestConventionPlugin.kt @@ -0,0 +1,33 @@ +package test + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.withType +import utils.catalog +import utils.testImplementation +import utils.testRuntimeOnly + +/** + * androidTest를 하지 않는 모듈에서 공통으로 사용하는 unitTest 라이브러리 모음 컨벤션 플러그인 + */ +class CommonUnitTestConventionPlugin: Plugin { + + override fun apply(target: Project) { + target.run { + dependencies { + testImplementation(catalog.findBundle("test-unit").get()) + testImplementation(catalog.findBundle("test-coroutine").get()) + testImplementation(catalog.findBundle("kotest").get()) + testRuntimeOnly(catalog.findBundle("test-runtime").get()) + + testImplementation(catalog.findLibrary("mockk").get()) + } + + tasks.withType { + useJUnitPlatform() + } + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/test/FeatureTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/test/FeatureTestConventionPlugin.kt new file mode 100644 index 000000000..d0b4221f3 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/test/FeatureTestConventionPlugin.kt @@ -0,0 +1,49 @@ +package test + +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.withType +import utils.androidTestImplementation +import utils.catalog +import utils.testImplementation +import utils.testRuntimeOnly + +/** + * Feature 모듈에서 공통으로 사용하는 테스트 라이브러리 모음 컨벤션 플러그인 + * unitTest, androidTest 모두 포함 + */ +class FeatureTestConventionPlugin: Plugin { + + override fun apply(target: Project) { + target.run { + extensions.configure { + packaging { + resources { + excludes += "META-INF/LICENSE.md" + excludes += "META-INF/LICENSE-notice.md" + } + } + } + dependencies { + androidTestImplementation(catalog.findBundle("android-test").get()) + androidTestImplementation(catalog.findBundle("test-unit").get()) + testImplementation(catalog.findBundle("test-unit").get()) + testImplementation(catalog.findBundle("test-coroutine").get()) + testImplementation(catalog.findBundle("orbit-test").get()) + testImplementation(catalog.findBundle("kotest").get()) + testRuntimeOnly(catalog.findBundle("test-runtime").get()) + + testImplementation(catalog.findLibrary("mockk").get()) + androidTestImplementation(catalog.findLibrary("mockk-android").get()) + } + + tasks.withType { + useJUnitPlatform() + } + } + } +} \ No newline at end of file diff --git a/build-logic/convention/src/main/kotlin/utils/DependencyExtensions.kt b/build-logic/convention/src/main/kotlin/utils/DependencyExtensions.kt index 8c1876bad..5ae21e9cf 100644 --- a/build-logic/convention/src/main/kotlin/utils/DependencyExtensions.kt +++ b/build-logic/convention/src/main/kotlin/utils/DependencyExtensions.kt @@ -28,4 +28,8 @@ fun DependencyHandler.androidTestImplementation(dependency: Any) { fun DependencyHandler.testImplementation(dependency: Any) { add("testImplementation", dependency) +} + +fun DependencyHandler.testRuntimeOnly(dependency: Any) { + add("testRuntimeOnly", dependency) } \ No newline at end of file diff --git a/core/ads-api/build.gradle.kts b/core/ads-api/build.gradle.kts deleted file mode 100644 index 238e6fa39..000000000 --- a/core/ads-api/build.gradle.kts +++ /dev/null @@ -1,8 +0,0 @@ -plugins { - alias(libs.plugins.acon.android.library) - alias(libs.plugins.acon.android.library.compose) -} - -android { - namespace = "com.acon.core.ads_api" -} \ No newline at end of file diff --git a/core/ads-api/src/main/AndroidManifest.xml b/core/ads-api/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e68a..000000000 --- a/core/ads-api/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/core/ads-api/src/main/java/com/acon/acon/core/ads_api/AdProvider.kt b/core/ads-api/src/main/java/com/acon/acon/core/ads_api/AdProvider.kt deleted file mode 100644 index 03007b431..000000000 --- a/core/ads-api/src/main/java/com/acon/acon/core/ads_api/AdProvider.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.acon.acon.core.ads_api - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.Modifier - -interface AdProvider { - - @Composable - fun NativeAd(modifier: Modifier) -} - -val LocalSpotListAdProvider = staticCompositionLocalOf { - error("AdProvider가 제공되지 않았습니다.") -} \ No newline at end of file diff --git a/core/ads-api/.gitignore b/core/ads/.gitignore similarity index 100% rename from core/ads-api/.gitignore rename to core/ads/.gitignore diff --git a/provider/ads-impl/build.gradle.kts b/core/ads/build.gradle.kts similarity index 89% rename from provider/ads-impl/build.gradle.kts rename to core/ads/build.gradle.kts index a64850e22..bca35620d 100644 --- a/provider/ads-impl/build.gradle.kts +++ b/core/ads/build.gradle.kts @@ -1,4 +1,5 @@ import java.util.Properties +import kotlin.apply plugins { alias(libs.plugins.acon.android.library) @@ -12,7 +13,7 @@ val localProperties = Properties().apply { } android { - namespace = "com.acon.feature.ads_impl" + namespace = "com.acon.core.ads" defaultConfig { buildConfigField("String", "NATIVE_ADMOB_ID", "\"${localProperties["native_admob_id"]}\"") @@ -21,7 +22,6 @@ android { } dependencies { - implementation(projects.core.adsApi) implementation(projects.core.designsystem) implementation(libs.google.services.ads) diff --git a/core/ads-api/consumer-rules.pro b/core/ads/consumer-rules.pro similarity index 100% rename from core/ads-api/consumer-rules.pro rename to core/ads/consumer-rules.pro diff --git a/core/ads-api/proguard-rules.pro b/core/ads/proguard-rules.pro similarity index 100% rename from core/ads-api/proguard-rules.pro rename to core/ads/proguard-rules.pro diff --git a/provider/ads-impl/src/main/AndroidManifest.xml b/core/ads/src/main/AndroidManifest.xml similarity index 74% rename from provider/ads-impl/src/main/AndroidManifest.xml rename to core/ads/src/main/AndroidManifest.xml index ec0cec507..b9ed92ccb 100644 --- a/provider/ads-impl/src/main/AndroidManifest.xml +++ b/core/ads/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - \ No newline at end of file diff --git a/provider/ads-impl/src/main/java/com/acon/acon/provider/ads_impl/SpotListAdProvider.kt b/core/ads/src/main/java/com/acon/core/ads/SpotListAdProvider.kt similarity index 94% rename from provider/ads-impl/src/main/java/com/acon/acon/provider/ads_impl/SpotListAdProvider.kt rename to core/ads/src/main/java/com/acon/core/ads/SpotListAdProvider.kt index 905f70353..d3c7dad5c 100644 --- a/provider/ads-impl/src/main/java/com/acon/acon/provider/ads_impl/SpotListAdProvider.kt +++ b/core/ads/src/main/java/com/acon/core/ads/SpotListAdProvider.kt @@ -1,7 +1,8 @@ -package com.acon.acon.provider.ads_impl +package com.acon.core.ads import android.annotation.SuppressLint import android.view.Gravity +import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.TextView @@ -35,11 +36,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.zIndex import coil3.compose.AsyncImage +import com.acon.acon.core.designsystem.R import com.acon.acon.core.designsystem.component.button.v2.AconFilledButton import com.acon.acon.core.designsystem.effect.imageGradientLayer import com.acon.acon.core.designsystem.theme.AconTheme -import com.acon.acon.core.ads_api.AdProvider -import com.acon.feature.ads_impl.BuildConfig import com.google.android.gms.ads.AdListener import com.google.android.gms.ads.AdLoader import com.google.android.gms.ads.AdRequest @@ -50,19 +50,12 @@ import com.google.android.gms.ads.nativead.NativeAd import com.google.android.gms.ads.nativead.NativeAdOptions import com.google.android.gms.ads.nativead.NativeAdView -class SpotListAdProvider : AdProvider { - - @Composable - override fun NativeAd(modifier: Modifier) { - SpotListNativeAd(modifier) - } -} - @SuppressLint("MissingPermission") @Composable -private fun SpotListNativeAd(modifier: Modifier) { +fun SpotListNativeAd(modifier: Modifier) { val context = LocalContext.current var adUiState by remember { mutableStateOf(AdUiState.Loading) } + val callToActionClickTrigger = remember { View(context) } DisposableEffect(Unit) { val adLoader = AdLoader.Builder(context, BuildConfig.NATIVE_ADMOB_ID) @@ -122,6 +115,9 @@ private fun SpotListNativeAd(modifier: Modifier) { } layout.addView(adChoicesView, adChoicesLayoutParams) + layout.addView(callToActionClickTrigger) + nativeAdView.callToActionView = callToActionClickTrigger + nativeAdView.addView(layout) nativeAdView.setNativeAd(ad) nativeAdView @@ -151,7 +147,7 @@ private fun SpotListNativeAd(modifier: Modifier) { contentAlignment = Alignment.Center ) { Text( - text = stringResource(com.acon.acon.core.designsystem.R.string.advertisement), + text = stringResource(R.string.advertisement), style = AconTheme.typography.Body1, color = AconTheme.color.White, fontWeight = FontWeight.W400, @@ -179,7 +175,7 @@ private fun SpotListNativeAd(modifier: Modifier) { ad.callToAction?.let { AconFilledButton( modifier = Modifier, - onClick = {}, + onClick = callToActionClickTrigger::performClick, contentPadding = PaddingValues( horizontal = 23.dp, vertical = 8.dp diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index 133bf0e94..238d54d35 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -19,7 +19,4 @@ android { dependencies { implementation(libs.amplitude) - - testImplementation(libs.bundles.android.test) - testImplementation(libs.bundles.non.android.test) } \ No newline at end of file diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 64f4f837e..adfa1dd7f 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -1,10 +1,9 @@ plugins { alias(libs.plugins.acon.non.android.library) + alias(libs.plugins.acon.common.unit.test) } dependencies { implementation(libs.javax.inject) implementation(libs.kotlinx.coroutines.core) - - testImplementation(libs.bundles.non.android.test) } \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index d6369b6a2..4240087cc 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.acon.android.library.hilt) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.protobuf) + alias(libs.plugins.acon.common.unit.test) } val localProperties = Properties().apply { @@ -76,10 +77,6 @@ dependencies { implementation(libs.preferences.datastore) implementation(libs.proto.datastore) implementation(libs.protobuf.kotlin) - - testImplementation(libs.bundles.non.android.test) - testRuntimeOnly(libs.bundles.junit5.runtime) - androidTestImplementation(libs.bundles.android.test) } tasks.withType { diff --git a/core/data/src/androidTest/java/com/acon/core/data/ExampleInstrumentedTest.kt b/core/data/src/androidTest/java/com/acon/core/data/ExampleInstrumentedTest.kt deleted file mode 100644 index b0610e6cb..000000000 --- a/core/data/src/androidTest/java/com/acon/core/data/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.acon.core.data - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.acon.core.data.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/AconAppApi.kt b/core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/AconAppApi.kt new file mode 100644 index 000000000..71f439e82 --- /dev/null +++ b/core/data/src/main/kotlin/com/acon/core/data/api/remote/auth/AconAppApi.kt @@ -0,0 +1,14 @@ +package com.acon.core.data.api.remote.auth + +import com.acon.core.data.dto.request.GetPresignedUrlRequest +import com.acon.core.data.dto.response.PresignedUrlResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface AconAppApi { + + @POST("/api/v1/images/presigned-url") + suspend fun getPresignedUrl( + @Body request: GetPresignedUrlRequest + ): PresignedUrlResponse +} \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/api/remote/noauth/AconAppNoAuthApi.kt b/core/data/src/main/kotlin/com/acon/core/data/api/remote/noauth/AconAppNoAuthApi.kt index c5ba974c9..15df9e71f 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/api/remote/noauth/AconAppNoAuthApi.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/api/remote/noauth/AconAppNoAuthApi.kt @@ -1,11 +1,7 @@ package com.acon.core.data.api.remote.noauth -import com.acon.core.data.dto.request.GetPresignedUrlRequest -import com.acon.core.data.dto.response.PresignedUrlResponse import com.acon.core.data.dto.response.app.ShouldUpdateResponse -import retrofit2.http.Body import retrofit2.http.GET -import retrofit2.http.POST import retrofit2.http.Query interface AconAppNoAuthApi { @@ -14,9 +10,4 @@ interface AconAppNoAuthApi { @Query("version") currentVersion: String, @Query("platform") platform: String = "android" ): ShouldUpdateResponse - - @POST("/api/v1/images/presigned-url") - suspend fun getPresignedUrl( - @Body request: GetPresignedUrlRequest - ): PresignedUrlResponse } \ No newline at end of file diff --git a/core/data/src/main/kotlin/com/acon/core/data/datasource/local/OnboardingLocalDataSource.kt b/core/data/src/main/kotlin/com/acon/core/data/datasource/local/OnboardingLocalDataSource.kt index af9818f0b..323ef1134 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/datasource/local/OnboardingLocalDataSource.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/datasource/local/OnboardingLocalDataSource.kt @@ -14,8 +14,8 @@ class OnboardingLocalDataSource @Inject constructor( onboardingDataStore.updateData { prefs -> prefs.copy { shouldShowIntroduce = pref.shouldShowIntroduce - hasTastePreference = pref.hasTastePreference - hasVerifiedArea = pref.hasVerifiedArea + shouldChooseDislikes = pref.shouldChooseDislikes + shouldVerifyArea = pref.shouldVerifyArea } } } @@ -28,18 +28,18 @@ class OnboardingLocalDataSource @Inject constructor( } } - suspend fun updateHasPreference(hasPreference: Boolean) { + suspend fun updateShouldChooseDislikes(shouldChooseDislikes: Boolean) { onboardingDataStore.updateData { prefs -> prefs.copy { - this.hasTastePreference = hasPreference + this.shouldChooseDislikes = shouldChooseDislikes } } } - suspend fun updateHasVerifiedArea(verifiedArea: Boolean) { + suspend fun updateShouldVerifyArea(shouldVerifyArea: Boolean) { onboardingDataStore.updateData { prefs -> prefs.copy { - this.hasVerifiedArea = verifiedArea + this.shouldVerifyArea = shouldVerifyArea } } } diff --git a/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/AconAppRemoteDataSource.kt b/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/AconAppRemoteDataSource.kt index 74b562f6f..833cbad3b 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/AconAppRemoteDataSource.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/datasource/remote/AconAppRemoteDataSource.kt @@ -1,5 +1,6 @@ package com.acon.core.data.datasource.remote +import com.acon.core.data.api.remote.auth.AconAppApi import com.acon.core.data.dto.response.app.ShouldUpdateResponse import com.acon.core.data.api.remote.noauth.AconAppNoAuthApi import com.acon.core.data.api.remote.noauth.FileUploadApi @@ -10,6 +11,7 @@ import javax.inject.Inject class AconAppRemoteDataSource @Inject constructor( private val aconAppNoAuthApi: AconAppNoAuthApi, + private val aconAppApi: AconAppApi, private val fileUploadApi: FileUploadApi, ) { suspend fun fetchShouldUpdateApp(currentVersion: String): ShouldUpdateResponse { @@ -17,7 +19,7 @@ class AconAppRemoteDataSource @Inject constructor( } suspend fun getPresignedUrl(request: GetPresignedUrlRequest): PresignedUrlResponse { - return aconAppNoAuthApi.getPresignedUrl(request) + return aconAppApi.getPresignedUrl(request) } suspend fun uploadFile(presignedUrl: String, body: RequestBody) { 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 9770727dd..806afff8b 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 @@ -8,6 +8,7 @@ import com.acon.core.data.api.remote.noauth.AconAppNoAuthApi 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.AconAppApi import com.acon.core.data.api.remote.auth.OnboardingAuthApi import com.acon.core.data.api.remote.auth.SpotAuthApi import com.acon.core.data.api.remote.noauth.SpotNoAuthApi @@ -100,12 +101,20 @@ internal object ApiModule { @Singleton @Provides - fun providesAconAppApi( + fun providesAconAppNoAuthApi( @NoAuth retrofit: Retrofit ): AconAppNoAuthApi { return retrofit.create(AconAppNoAuthApi::class.java) } + @Singleton + @Provides + fun providesAconAppApi( + @Auth retrofit: Retrofit + ): AconAppApi { + return retrofit.create(AconAppApi::class.java) + } + @Singleton @Provides fun providesFileUploadApi( diff --git a/core/data/src/main/kotlin/com/acon/core/data/dto/response/SignInResponse.kt b/core/data/src/main/kotlin/com/acon/core/data/dto/response/SignInResponse.kt index de6135dc4..f0cb772c3 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/dto/response/SignInResponse.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/dto/response/SignInResponse.kt @@ -1,6 +1,5 @@ package com.acon.core.data.dto.response -import com.acon.acon.core.model.model.user.VerificationStatus import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -11,10 +10,4 @@ data class SignInResponse( @SerialName("refreshToken") val refreshToken: String?, @SerialName("hasVerifiedArea") val hasVerifiedArea: Boolean, @SerialName("hasPreference") val hasPreference: Boolean -) { - fun toVerificationStatus() = VerificationStatus( - externalUUID = externalUUID, - hasVerifiedArea = hasVerifiedArea, - hasPreference = hasPreference - ) -} +) \ 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 0fb4ce209..3be1b85a3 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,22 +1,29 @@ package com.acon.core.data.repository import android.content.Context +import android.net.Uri import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile +import com.acon.acon.core.common.IODispatcher 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.datasource.remote.AconAppRemoteDataSource import com.acon.core.data.dto.request.GetPresignedUrlRequest +import com.acon.core.data.dto.response.PresignedUrlResponse +import com.acon.core.data.error.runCatchingWith import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import javax.inject.Inject -import kotlin.collections.contains class AconAppRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, + @IODispatcher private val dispatcher: CoroutineDispatcher, private val aconAppRemoteDataSource: AconAppRemoteDataSource ) : AconAppRepository { @@ -33,25 +40,46 @@ class AconAppRepositoryImpl @Inject constructor( 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 - )) + return@runCatchingWith withContext(dispatcher) { + val presignedUrlResponseDeferred = async { + contentUri.getPresignedUrlResponse(imageType) + } + val requestBodyDeferred = async { + contentUri.getRequestBody() + } + + val presignedUrlResponse = presignedUrlResponseDeferred.await() + val requestBody = requestBodyDeferred.await() + aconAppRemoteDataSource.uploadFile( + presignedUrlResponse.presignedUrl, + requestBody + ) - val uriMimeType = context.contentResolver.getType(contentUri) - val finalMimeType = if (availableImageMimeTypes.contains(uriMimeType)) uriMimeType!! else "image/jpeg" + return@withContext presignedUrlResponse.fileUrl + } + } + } - val inputStream = context.contentResolver.openInputStream(contentUri) - val requestBody = inputStream?.use { input -> - input.readBytes().toRequestBody(finalMimeType.toMediaTypeOrNull()) - } ?: error("Failed to read image content: $url") + private suspend fun Uri.getPresignedUrlResponse(imageType: ImageType): PresignedUrlResponse { + val fileName = DocumentFile.fromSingleUri(context, this)?.name + ?: error("Failed to read file name: $this") - aconAppRemoteDataSource.uploadFile(presignedUrlResponse.presignedUrl, requestBody) + return aconAppRemoteDataSource.getPresignedUrl( + GetPresignedUrlRequest( + imageType = imageType, + fileName = fileName + ) + ) + } - return@runCatchingWith presignedUrlResponse.fileUrl - } + private fun Uri.getRequestBody(): RequestBody { + val uriMimeType = context.contentResolver.getType(this) + val finalMimeType = if (availableImageMimeTypes.contains(uriMimeType)) uriMimeType!! else "image/jpeg" + + val inputStream = context.contentResolver.openInputStream(this) + return inputStream?.use { input -> + input.readBytes().toRequestBody(finalMimeType.toMediaTypeOrNull()) + } ?: error("Failed to read image content: $this") } } \ 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 c31151cb4..38e3d3e31 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 @@ -25,7 +25,7 @@ class OnboardingRepositoryImpl @Inject constructor( return runCatchingWith(PostTastePreferenceResultError()) { val request = TastePreferenceRequest(dislikeFoods = dislikeFoods.map { it.name }) onboardingRemoteDataSource.submitTastePreferenceResult(request) - onboardingLocalDataSource.updateHasPreference(true) + onboardingLocalDataSource.updateShouldChooseDislikes(false) } } @@ -37,13 +37,13 @@ class OnboardingRepositoryImpl @Inject constructor( latitude = latitude, longitude = longitude ) - onboardingLocalDataSource.updateHasVerifiedArea(true) + onboardingLocalDataSource.updateShouldVerifyArea(false) areaDataStream.notifyDataChanged() } - override suspend fun updateHasTastePreference(hasPreference: Boolean): Result { + override suspend fun updateShouldChooseDislikes(shouldChoose: Boolean): Result { return runCatchingWith { - onboardingLocalDataSource.updateHasPreference(hasPreference) + onboardingLocalDataSource.updateShouldChooseDislikes(shouldChoose) } } @@ -53,9 +53,9 @@ class OnboardingRepositoryImpl @Inject constructor( } } - override suspend fun updateHasVerifiedArea(hasVerifiedArea: Boolean): Result { + override suspend fun updateShouldVerifyArea(shouldVerify: Boolean): Result { return runCatchingWith { - onboardingLocalDataSource.updateHasVerifiedArea(hasVerifiedArea) + onboardingLocalDataSource.updateShouldVerifyArea(shouldVerify) } } @@ -64,8 +64,8 @@ class OnboardingRepositoryImpl @Inject constructor( val entity = onboardingLocalDataSource.getOnboardingPreferences() OnboardingPreferences( shouldShowIntroduce = entity.shouldShowIntroduce, - hasTastePreference = entity.hasTastePreference, - hasVerifiedArea = entity.hasVerifiedArea + shouldChooseDislikes = entity.shouldChooseDislikes, + shouldVerifyArea = entity.shouldVerifyArea ) } } 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 050e06fa9..c4a599b0d 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 @@ -2,7 +2,7 @@ package com.acon.core.data.repository import com.acon.acon.core.model.model.user.CredentialCode import com.acon.acon.core.model.model.user.SocialPlatform -import com.acon.acon.core.model.model.user.VerificationStatus +import com.acon.acon.core.model.model.user.ExternalUUID import com.acon.acon.data.dto.request.DeleteAccountRequest import com.acon.acon.domain.error.user.PostSignInError import com.acon.acon.domain.error.user.PostSignOutError @@ -33,7 +33,7 @@ class UserRepositoryImpl @Inject constructor( override suspend fun signIn( socialType: SocialPlatform, code: CredentialCode - ): Result { + ): Result { return runCatchingWith(PostSignInError()) { val signInResponse = userRemoteDataSource.signIn( SignInRequest( @@ -48,17 +48,24 @@ class UserRepositoryImpl @Inject constructor( ) coroutineScope { - val verifiedAreaJob = async { - onboardingRepository.updateHasVerifiedArea(signInResponse.toVerificationStatus().hasVerifiedArea) + val shouldVerifyAreaJob = async { + onboardingRepository.updateShouldVerifyArea(!signInResponse.hasVerifiedArea) } - val tastePreferenceJob = async { - onboardingRepository.updateHasTastePreference(signInResponse.toVerificationStatus().hasPreference) + val shouldChooseDislikesJob = async { + onboardingRepository.updateShouldChooseDislikes(!signInResponse.hasPreference) } - awaitAll(verifiedAreaJob, tastePreferenceJob) + val shouldShowIntroduceJob = async { + onboardingRepository.updateShouldShowIntroduce( + (onboardingRepository.getOnboardingPreferences().getOrNull()?.shouldShowIntroduce == true) + && !signInResponse.hasVerifiedArea + && !signInResponse.hasPreference + ) + } + awaitAll(shouldVerifyAreaJob, shouldChooseDislikesJob, shouldShowIntroduceJob) } - signInResponse.toVerificationStatus() + ExternalUUID(signInResponse.externalUUID) } } @@ -70,8 +77,8 @@ class UserRepositoryImpl @Inject constructor( ) }.onSuccess { profileLocalDataSource.clearCache() - onboardingRepository.updateHasVerifiedArea(false) - onboardingRepository.updateHasTastePreference(false) + onboardingRepository.updateShouldVerifyArea(true) + onboardingRepository.updateShouldChooseDislikes(true) clearSession() } } @@ -86,8 +93,9 @@ class UserRepositoryImpl @Inject constructor( ) ) }.onSuccess { - onboardingRepository.updateHasVerifiedArea(false) - onboardingRepository.updateHasTastePreference(false) + profileLocalDataSource.clearCache() + onboardingRepository.updateShouldVerifyArea(true) + onboardingRepository.updateShouldChooseDislikes(true) clearSession() } } diff --git a/core/data/src/main/kotlin/com/acon/core/data/serializer/OnboardingPreferencesSerializer.kt b/core/data/src/main/kotlin/com/acon/core/data/serializer/OnboardingPreferencesSerializer.kt index 917bad6df..7798424da 100644 --- a/core/data/src/main/kotlin/com/acon/core/data/serializer/OnboardingPreferencesSerializer.kt +++ b/core/data/src/main/kotlin/com/acon/core/data/serializer/OnboardingPreferencesSerializer.kt @@ -13,8 +13,8 @@ class OnboardingPreferencesSerializer @Inject constructor() : Serializer원하시는 결과를 보여드리지 못해 죄송해요 대신 여기는 어떠세요? 다음에 들어오실 땐,\n꼭 찾아서 추천드릴게요 - 장소 등록 신청하기 + 장소 직접 등록하기 도보 %d분 길찾기 자전거로 %d분 길찾기 지금 위치 기준으로 다시 추천 받기 diff --git a/core/model/src/main/java/com/acon/acon/core/model/model/OnboardingPreferences.kt b/core/model/src/main/java/com/acon/acon/core/model/model/OnboardingPreferences.kt index 3dc15eb65..72d39817b 100644 --- a/core/model/src/main/java/com/acon/acon/core/model/model/OnboardingPreferences.kt +++ b/core/model/src/main/java/com/acon/acon/core/model/model/OnboardingPreferences.kt @@ -5,6 +5,6 @@ import kotlinx.serialization.Serializable @Serializable data class OnboardingPreferences( val shouldShowIntroduce: Boolean, - val hasTastePreference: Boolean, - val hasVerifiedArea: Boolean + val shouldChooseDislikes: Boolean, + val shouldVerifyArea: Boolean ) diff --git a/core/model/src/main/java/com/acon/acon/core/model/model/user/ExternalUUID.kt b/core/model/src/main/java/com/acon/acon/core/model/model/user/ExternalUUID.kt new file mode 100644 index 000000000..50519d899 --- /dev/null +++ b/core/model/src/main/java/com/acon/acon/core/model/model/user/ExternalUUID.kt @@ -0,0 +1,6 @@ +package com.acon.acon.core.model.model.user + +@JvmInline +value class ExternalUUID( + val value: String, +) \ No newline at end of file diff --git a/core/model/src/main/java/com/acon/acon/core/model/model/user/VerificationStatus.kt b/core/model/src/main/java/com/acon/acon/core/model/model/user/VerificationStatus.kt deleted file mode 100644 index e0920e5e8..000000000 --- a/core/model/src/main/java/com/acon/acon/core/model/model/user/VerificationStatus.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.acon.acon.core.model.model.user - -data class VerificationStatus( - val externalUUID: String, - val hasVerifiedArea: Boolean, - val hasPreference: Boolean -) \ No newline at end of file diff --git a/core/social/build.gradle.kts b/core/social/build.gradle.kts index a0fe0b961..2ad850837 100644 --- a/core/social/build.gradle.kts +++ b/core/social/build.gradle.kts @@ -4,6 +4,7 @@ import kotlin.apply plugins { alias(libs.plugins.acon.android.library) alias(libs.plugins.acon.android.library.hilt) + alias(libs.plugins.acon.common.unit.test) } val localProperties = Properties().apply { @@ -23,9 +24,6 @@ dependencies { implementation(projects.core.model) implementation(libs.bundles.googleSignIn) - - testImplementation(libs.bundles.non.android.test) - testRuntimeOnly(libs.bundles.junit5.runtime) } tasks.withType { diff --git a/core/social/src/androidTest/java/com/acon/core/social/ExampleInstrumentedTest.kt b/core/social/src/androidTest/java/com/acon/core/social/ExampleInstrumentedTest.kt deleted file mode 100644 index ed0b861aa..000000000 --- a/core/social/src/androidTest/java/com/acon/core/social/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.acon.core.social - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.acon.core.social.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 25fae7b83..6c0273c1d 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.acon.non.android.library) + alias(libs.plugins.acon.common.unit.test) } dependencies { @@ -7,8 +8,6 @@ dependencies { implementation(libs.javax.inject) implementation(libs.kotlinx.coroutines.core) - - testImplementation(libs.bundles.non.android.test) } tasks.withType { 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 0e71e6658..0d8b93967 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 @@ -4,5 +4,14 @@ import com.acon.acon.core.model.type.ImageType interface AconAppRepository { suspend fun shouldUpdateApp(currentVersion: String): Result + + /** + * 이미지를 서버에 업로드 + * + * @param imageType 업로드할 이미지 유형 + * @param url 이미지 파일의 로컬 경로 + * @return 업로드된 이미지의 최종 URL + * @see ImageType + */ 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/OnboardingRepository.kt b/domain/src/main/java/com/acon/acon/domain/repository/OnboardingRepository.kt index dc3383e46..82c4252f9 100644 --- a/domain/src/main/java/com/acon/acon/domain/repository/OnboardingRepository.kt +++ b/domain/src/main/java/com/acon/acon/domain/repository/OnboardingRepository.kt @@ -13,9 +13,9 @@ interface OnboardingRepository { longitude: Double ): Result - suspend fun updateHasTastePreference(hasPreference: Boolean): Result suspend fun updateShouldShowIntroduce(shouldShow: Boolean): Result - suspend fun updateHasVerifiedArea(hasVerifiedArea: Boolean): Result + suspend fun updateShouldChooseDislikes(shouldChoose: Boolean): Result + suspend fun updateShouldVerifyArea(shouldVerify: Boolean): Result suspend fun getOnboardingPreferences(): Result } diff --git a/domain/src/main/java/com/acon/acon/domain/repository/UserRepository.kt b/domain/src/main/java/com/acon/acon/domain/repository/UserRepository.kt index e38526569..7ed0b2444 100644 --- a/domain/src/main/java/com/acon/acon/domain/repository/UserRepository.kt +++ b/domain/src/main/java/com/acon/acon/domain/repository/UserRepository.kt @@ -2,12 +2,12 @@ package com.acon.acon.domain.repository import com.acon.acon.core.model.model.user.CredentialCode import com.acon.acon.core.model.model.user.SocialPlatform -import com.acon.acon.core.model.model.user.VerificationStatus +import com.acon.acon.core.model.model.user.ExternalUUID import com.acon.acon.core.model.type.SignInStatus import kotlinx.coroutines.flow.Flow interface UserRepository { - suspend fun signIn(socialType: SocialPlatform, code: CredentialCode): Result + suspend fun signIn(socialType: SocialPlatform, code: CredentialCode): Result suspend fun signOut(): Result suspend fun deleteAccount(reason: String): Result suspend fun clearSession(): Result diff --git a/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreen.kt b/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreen.kt index c65a0a4f3..702985e63 100644 --- a/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreen.kt +++ b/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/AreaVerificationScreen.kt @@ -16,16 +16,20 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.paint +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +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.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -42,7 +46,8 @@ internal fun AreaVerificationScreen( state: AreaVerificationState, onNextButtonClick: () -> Unit, modifier: Modifier = Modifier, - onSkipClick: () -> Unit = {} + onSkipClick: () -> Unit = {}, + onBack: () -> Unit = {} ) { val screenHeightDp = getScreenHeight() val offsetY = (screenHeightDp * 0.65f) @@ -80,23 +85,32 @@ internal fun AreaVerificationScreen( } .padding(8.dp) ) + + Text( + text = stringResource(R.string.alert_about_skip_area_verification), + style = AconTheme.typography.Body1, + color = AconTheme.color.Gray500, + fontWeight = FontWeight.W400, + modifier = Modifier + .align(Alignment.TopCenter) + .padding( + top = 96.dp + ) + .graphicsLayer { + alpha = skipAlertTextAlpha + }, + textAlign = TextAlign.Center + ) + } else { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_topbar_arrow_left), + contentDescription = stringResource(R.string.back), + modifier = Modifier.padding(start = 16.dp, top = 32.dp).noRippleClickable { + onBack() + }, tint = Color.Unspecified + ) } - Text( - text = stringResource(R.string.alert_about_skip_area_verification), - style = AconTheme.typography.Body1, - color = AconTheme.color.Gray500, - fontWeight = FontWeight.W400, - modifier = Modifier - .align(Alignment.TopCenter) - .padding( - top = 96.dp - ) - .graphicsLayer { - alpha = skipAlertTextAlpha - }, - textAlign = TextAlign.Center - ) Column( modifier = Modifier .fillMaxSize() 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 23237f7d5..189d92390 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 @@ -24,6 +24,7 @@ fun AreaVerificationScreenContainer( onNavigateToChooseDislikes: () -> Unit, onNavigateToIntroduce: () -> Unit, onNavigateToSpotList: () -> Unit, + onNavigateBack: () -> Unit, skippable: Boolean, modifier: Modifier = Modifier, viewModel: AreaVerificationViewModel = hiltViewModel(creationCallback = { factory: AreaVerificationViewModel.Factory -> @@ -46,7 +47,8 @@ fun AreaVerificationScreenContainer( } }, modifier = modifier, - onSkipClick = viewModel::onSkipClicked + onSkipClick = viewModel::onSkipClicked, + onBack = viewModel::onBackClicked ) viewModel.useLiveLocation() @@ -77,6 +79,7 @@ fun AreaVerificationScreenContainer( is AreaVerificationSideEffect.NavigateToChooseDislikes -> onNavigateToChooseDislikes() is AreaVerificationSideEffect.NavigateToIntroduce -> onNavigateToIntroduce() is AreaVerificationSideEffect.NavigateToSpotList -> onNavigateToSpotList() + is AreaVerificationSideEffect.NavigateBack -> onNavigateBack() } } diff --git a/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/VerifyInMapScreenContainer.kt b/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/VerifyInMapScreenContainer.kt index 6686ff619..c23c7e395 100644 --- a/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/VerifyInMapScreenContainer.kt +++ b/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/composable/VerifyInMapScreenContainer.kt @@ -4,7 +4,6 @@ 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.core.model.model.OnboardingPreferences import com.acon.feature.onboarding.area.viewmodel.VerifyInMapSideEffect import com.acon.feature.onboarding.area.viewmodel.VerifyInMapViewModel import org.orbitmvi.orbit.compose.collectAsState 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 5536c2f73..5346713dc 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 @@ -21,7 +21,9 @@ class AreaVerificationViewModel @AssistedInject constructor( AreaVerificationState( shouldShowSkipButton = shouldShowSkipButton ) - ) + ) { + onboardingRepository.updateShouldVerifyArea(false) + } fun onNextButtonClick() = intent { postSideEffect( @@ -33,17 +35,19 @@ class AreaVerificationViewModel @AssistedInject constructor( timeRepository.saveUserActionTime(UserActionType.SKIP_AREA_VERIFICATION, System.currentTimeMillis()) onboardingRepository.getOnboardingPreferences().onSuccess { pref -> - if (pref.hasTastePreference.not()) + if (pref.shouldChooseDislikes) postSideEffect(AreaVerificationSideEffect.NavigateToChooseDislikes) - else if (pref.shouldShowIntroduce) - postSideEffect(AreaVerificationSideEffect.NavigateToIntroduce) else postSideEffect(AreaVerificationSideEffect.NavigateToSpotList) }.onFailure { - postSideEffect(AreaVerificationSideEffect.NavigateToChooseDislikes) + postSideEffect(AreaVerificationSideEffect.NavigateToSpotList) } } + fun onBackClicked() = intent { + postSideEffect(AreaVerificationSideEffect.NavigateBack) + } + @AssistedFactory interface Factory { fun create(shouldShowSkipButton: Boolean): AreaVerificationViewModel @@ -71,4 +75,5 @@ sealed interface AreaVerificationSideEffect { data object NavigateToChooseDislikes : AreaVerificationSideEffect data object NavigateToIntroduce : AreaVerificationSideEffect data object NavigateToSpotList : AreaVerificationSideEffect + data object NavigateBack : AreaVerificationSideEffect } \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/viewmodel/VerifyInMapViewModel.kt b/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/viewmodel/VerifyInMapViewModel.kt index 7632c38ae..121670d3c 100644 --- a/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/viewmodel/VerifyInMapViewModel.kt +++ b/feature/onboarding/src/main/java/com/acon/feature/onboarding/area/viewmodel/VerifyInMapViewModel.kt @@ -1,6 +1,5 @@ package com.acon.feature.onboarding.area.viewmodel -import com.acon.acon.core.model.model.OnboardingPreferences import com.acon.acon.core.ui.base.BaseContainerHost import com.acon.acon.domain.repository.OnboardingRepository import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/feature/onboarding/src/main/java/com/acon/feature/onboarding/dislikes/viewmodel/ChooseDislikesViewModel.kt b/feature/onboarding/src/main/java/com/acon/feature/onboarding/dislikes/viewmodel/ChooseDislikesViewModel.kt index db26e4f9b..b0bc1023a 100644 --- a/feature/onboarding/src/main/java/com/acon/feature/onboarding/dislikes/viewmodel/ChooseDislikesViewModel.kt +++ b/feature/onboarding/src/main/java/com/acon/feature/onboarding/dislikes/viewmodel/ChooseDislikesViewModel.kt @@ -18,7 +18,7 @@ class ChooseDislikesViewModel @Inject constructor( ) : BaseContainerHost() { override val container = container(ChooseDislikesUiState.Success()) { - + onboardingRepository.updateShouldChooseDislikes(false) } fun onNoneClicked() = intent { diff --git a/feature/onboarding/src/main/java/com/acon/feature/onboarding/introduce/composable/IntroduceScreenContainer.kt b/feature/onboarding/src/main/java/com/acon/feature/onboarding/introduce/composable/IntroduceScreenContainer.kt index d0245beef..80ae30567 100644 --- a/feature/onboarding/src/main/java/com/acon/feature/onboarding/introduce/composable/IntroduceScreenContainer.kt +++ b/feature/onboarding/src/main/java/com/acon/feature/onboarding/introduce/composable/IntroduceScreenContainer.kt @@ -16,6 +16,8 @@ import org.orbitmvi.orbit.compose.collectSideEffect @Composable fun IntroduceScreenContainer( onNavigateToHome: () -> Unit, + onNavigateToAreaVerification: () -> Unit, + onNavigateToChooseDislikes: () -> Unit, modifier: Modifier = Modifier, viewModel: IntroduceViewModel = hiltViewModel() ) { @@ -49,7 +51,9 @@ fun IntroduceScreenContainer( viewModel.collectSideEffect { sideEffect -> when (sideEffect) { - IntroduceSideEffect.OnNavigateToHomeScreen -> onNavigateToHome() + IntroduceSideEffect.NavigateToHomeScreen -> onNavigateToHome() + IntroduceSideEffect.NavigateToAreaVerification -> onNavigateToAreaVerification() + IntroduceSideEffect.NavigateToChooseDislikes -> onNavigateToChooseDislikes() } } } diff --git a/feature/onboarding/src/main/java/com/acon/feature/onboarding/introduce/viewmodel/IntroduceViewModel.kt b/feature/onboarding/src/main/java/com/acon/feature/onboarding/introduce/viewmodel/IntroduceViewModel.kt index 08c7a645a..483a279e1 100644 --- a/feature/onboarding/src/main/java/com/acon/feature/onboarding/introduce/viewmodel/IntroduceViewModel.kt +++ b/feature/onboarding/src/main/java/com/acon/feature/onboarding/introduce/viewmodel/IntroduceViewModel.kt @@ -1,20 +1,25 @@ package com.acon.feature.onboarding.introduce.viewmodel import androidx.compose.runtime.Immutable +import com.acon.acon.core.model.type.SignInStatus import com.acon.acon.core.ui.base.BaseContainerHost import com.acon.acon.domain.repository.OnboardingRepository +import com.acon.acon.domain.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject @HiltViewModel class IntroduceViewModel @Inject constructor( - private val onboardingRepository: OnboardingRepository + private val onboardingRepository: OnboardingRepository, + private val userRepository: UserRepository ) : BaseContainerHost() { override val container = container( initialState = IntroduceState() - ) + ) { + onboardingRepository.updateShouldShowIntroduce(false) + } fun onIntroduceLocalReviewScreenDisposed() = intent { reduce { @@ -41,8 +46,22 @@ class IntroduceViewModel @Inject constructor( } fun onStartButtonClicked() = intent { - onboardingRepository.updateShouldShowIntroduce(false) - postSideEffect(IntroduceSideEffect.OnNavigateToHomeScreen) + userRepository.getSignInStatus().collect { signInStatus -> + if(signInStatus == SignInStatus.USER) { + onboardingRepository.getOnboardingPreferences().onSuccess { pref -> + if (pref.shouldVerifyArea) + postSideEffect(IntroduceSideEffect.NavigateToAreaVerification) + else if (pref.shouldChooseDislikes) + postSideEffect(IntroduceSideEffect.NavigateToChooseDislikes) + else + postSideEffect(IntroduceSideEffect.NavigateToHomeScreen) + }.onFailure { + postSideEffect(IntroduceSideEffect.NavigateToHomeScreen) + } + } else { + postSideEffect(IntroduceSideEffect.NavigateToHomeScreen) + } + } } } @@ -54,5 +73,7 @@ data class IntroduceState( ) sealed interface IntroduceSideEffect { - data object OnNavigateToHomeScreen : IntroduceSideEffect + data object NavigateToAreaVerification: IntroduceSideEffect + data object NavigateToChooseDislikes : IntroduceSideEffect + data object NavigateToHomeScreen : IntroduceSideEffect } \ No newline at end of file diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts index 1a30936b2..60ae74189 100644 --- a/feature/profile/build.gradle.kts +++ b/feature/profile/build.gradle.kts @@ -1,5 +1,3 @@ -import utils.androidTestImplementation -import utils.testImplementation import java.util.Properties plugins { @@ -10,6 +8,7 @@ plugins { alias(libs.plugins.acon.android.library.orbit) alias(libs.plugins.acon.android.library.haze) alias(libs.plugins.acon.android.library.coil) + alias(libs.plugins.acon.feature.test) } val localProperties = Properties().apply { @@ -34,14 +33,6 @@ android { dependencies { implementation(libs.google.services.ads) // TODO - admob Plugin 분리? implementation(libs.androidx.paignig.compose) - - testImplementation(libs.bundles.non.android.test) - androidTestImplementation(libs.bundles.non.android.test) - androidTestImplementation(libs.mockk.android) - testRuntimeOnly(libs.bundles.junit5.runtime) - androidTestImplementation(libs.bundles.android.test) - testImplementation(libs.bundles.orbit.test) - testImplementation(libs.bundles.kotest) } tasks.withType { diff --git a/feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInViewModel.kt b/feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInViewModel.kt index 628778529..56c0ee563 100644 --- a/feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInViewModel.kt +++ b/feature/signin/src/main/java/com/acon/acon/feature/signin/screen/SignInViewModel.kt @@ -3,7 +3,7 @@ package com.acon.acon.feature.signin.screen 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.model.user.VerificationStatus +import com.acon.acon.core.model.model.user.ExternalUUID import com.acon.acon.core.model.type.SignInStatus import com.acon.acon.core.ui.base.BaseContainerHost import com.acon.acon.domain.repository.OnboardingRepository @@ -31,14 +31,15 @@ class SignInViewModel @Inject constructor( } } else { onboardingRepository.getOnboardingPreferences().onSuccess { - if(it.hasTastePreference.not()) + if (it.shouldShowIntroduce) + postSideEffect(SignInSideEffect.NavigateToIntroduce) + else if (it.shouldVerifyArea) + postSideEffect(SignInSideEffect.NavigateToAreaVerification) + else if (it.shouldChooseDislikes) postSideEffect(SignInSideEffect.NavigateToChooseDislikes) - else { - if (it.shouldShowIntroduce) - postSideEffect(SignInSideEffect.NavigateToIntroduce) - else - postSideEffect(SignInSideEffect.NavigateToSpotListView) - } + else + postSideEffect(SignInSideEffect.NavigateToSpotListView) + } } signInStatus.collectLatest { @@ -62,30 +63,34 @@ class SignInViewModel @Inject constructor( val platform = socialAuthClient.platform val code = socialAuthClient.getCredentialCode() - userRepository.signIn(platform, code ?: return@intent).onSuccess { verificationStatus -> - onSignInComplete(verificationStatus) + userRepository.signIn(platform, code ?: return@intent).onSuccess { externalUUID -> + onSignInComplete(externalUUID) }.onFailure { postSideEffect(SignInSideEffect.ShowToastMessage) } } - private fun onSignInComplete(verificationStatus: VerificationStatus) = intent { + private fun onSignInComplete(externalUUID: ExternalUUID) = intent { AconAmplitude.trackEvent( eventName = EventNames.SIGN_IN, properties = mapOf( PropertyKeys.SIGN_IN_OR_NOT to true ) ) - if (verificationStatus.hasVerifiedArea.not()) { - postSideEffect(SignInSideEffect.NavigateToAreaVerification) - } else if (verificationStatus.hasPreference.not()) { - postSideEffect(SignInSideEffect.NavigateToChooseDislikes) - } else if (onboardingRepository.getOnboardingPreferences().getOrNull()?.shouldShowIntroduce == true) { - postSideEffect(SignInSideEffect.NavigateToIntroduce) - } else { + onboardingRepository.getOnboardingPreferences().onSuccess { pref -> + if (pref.shouldShowIntroduce) { + postSideEffect(SignInSideEffect.NavigateToIntroduce) + } else if (pref.shouldVerifyArea) { + postSideEffect(SignInSideEffect.NavigateToAreaVerification) + } else if (pref.shouldChooseDislikes) { + postSideEffect(SignInSideEffect.NavigateToChooseDislikes) + } else { + postSideEffect(SignInSideEffect.NavigateToSpotListView) + } + }.onFailure { postSideEffect(SignInSideEffect.NavigateToSpotListView) } - AconAmplitude.setUserId(verificationStatus.externalUUID) + AconAmplitude.setUserId(externalUUID.value) } fun onClickTermsOfUse() = intent { diff --git a/feature/signin/src/main/java/com/acon/acon/feature/signin/screen/component/SignInTopBar.kt b/feature/signin/src/main/java/com/acon/acon/feature/signin/screen/component/SignInTopBar.kt index df3a0cb3c..2149f374e 100644 --- a/feature/signin/src/main/java/com/acon/acon/feature/signin/screen/component/SignInTopBar.kt +++ b/feature/signin/src/main/java/com/acon/acon/feature/signin/screen/component/SignInTopBar.kt @@ -31,7 +31,7 @@ fun SignInTopBar( Text( text = stringResource(R.string.signin_topbar_text), style = AconTheme.typography.Body1, - color = AconTheme.color.White, + color = AconTheme.color.Gray500, modifier = Modifier .padding(8.dp) .noRippleClickable { onClickText() } diff --git a/feature/spot/build.gradle.kts b/feature/spot/build.gradle.kts index 9d4647713..5b68a7cf8 100644 --- a/feature/spot/build.gradle.kts +++ b/feature/spot/build.gradle.kts @@ -15,7 +15,7 @@ android { dependencies { implementation(projects.core.map) - implementation(projects.core.adsApi) + implementation(projects.core.ads) implementation(libs.branch.io) implementation(libs.pulltorefresh) diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreenContainer.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreenContainer.kt index 142818a75..27baa6756 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreenContainer.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotdetail/composable/SpotDetailScreenContainer.kt @@ -7,10 +7,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import com.acon.acon.core.designsystem.R +import com.acon.acon.core.model.type.SignInStatus import com.acon.core.map.onLocationReady import com.acon.acon.core.ui.android.showToast import com.acon.acon.core.ui.compose.LocalOnRetry import com.acon.acon.core.ui.android.openNaverMapNavigationWithMode +import com.acon.acon.core.ui.compose.LocalRequestSignIn +import com.acon.acon.core.ui.compose.LocalSignInStatus import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -23,6 +26,8 @@ fun SpotDetailScreenContainer( ) { val context = LocalContext.current val state by viewModel.collectAsState() + val signInStatus = LocalSignInStatus.current + val onRequestSignIn = LocalRequestSignIn.current CompositionLocalProvider(LocalOnRetry provides viewModel::retry) { SpotDetailScreen( @@ -30,7 +35,13 @@ fun SpotDetailScreenContainer( modifier = modifier, onNavigateToBack = viewModel::navigateToBack, onBackToAreaVerification = onBackToAreaVerification, - onClickBookmark = viewModel::toggleBookmark, + onClickBookmark = { + if (signInStatus == SignInStatus.GUEST) { + onRequestSignIn("") + } else { + viewModel.toggleBookmark() + } + }, onClickRequestMenuBoard = viewModel::fetchMenuBoardList, onDismissMenuBoard = viewModel::onDismissMenuBoard, onRequestErrorReportModal = viewModel::onRequestReportErrorModal, 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 f6e8f1137..0e5a9f6b7 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 @@ -45,21 +45,7 @@ class SpotDetailViewModel @Inject constructor( override val container = container(SpotDetailUiState.Loading) { - signInStatus.collect { - when (it) { - SignInStatus.GUEST -> { - if (spotNavData.isFromDeepLink == true) { - fetchedSpotDetail() - } else { - reduce { SpotDetailUiState.LoadFailed() } - } - } - - else -> { - fetchedSpotDetail() - } - } - } + fetchedSpotDetail() } private fun fetchedSpotDetail() = intent { @@ -70,14 +56,14 @@ class SpotDetailViewModel @Inject constructor( val isDeepLink = spotNavData.isFromDeepLink == true val spotDetailDeferred = viewModelScope.async { - if (isDeepLink) { + if (signInStatus.value == SignInStatus.GUEST) { spotRepository.fetchSpotDetail( spotId = spotNavData.spotId, - isDeepLink = true + isDeepLink = isDeepLink ) } else { spotRepository.fetchSpotDetailFromUser( - spotId = spotNavData.spotId + spotId = spotNavData.spotId, ) } } diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt index f49a23fdf..17b068b04 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/SpotListViewModel.kt @@ -77,7 +77,7 @@ class SpotListViewModel @Inject constructor( var showAreaVerificationModal = false if (isCooldownExpiredUseCase(UserActionType.SKIP_AREA_VERIFICATION, 24 * 60 * 60) && signInStatus.value != SignInStatus.GUEST) { showAreaVerificationModal = onboardingRepository.getOnboardingPreferences() - .getOrNull()?.hasVerifiedArea == false + .getOrNull()?.shouldVerifyArea == true } fetchSpotList(location, Condition( @@ -350,6 +350,10 @@ class SpotListViewModel @Inject constructor( } } } + + fun onRegisterNewSpot() = intent { + postSideEffect(SpotListSideEffectV2.NavigateToUploadPlace) + } } sealed interface SpotListUiStateV2 { @@ -399,6 +403,7 @@ sealed interface SpotListSideEffectV2 { data object ShowToastMessage : SpotListSideEffectV2 data class NavigateToExternalMap(val handler: NavigationAppHandler) : SpotListSideEffectV2 data class NavigateToSpotDetailScreen(val spot: Spot, val transportMode: TransportMode) : SpotListSideEffectV2 + data object NavigateToUploadPlace : SpotListSideEffectV2 } internal typealias FilterDetailKey = KClass> diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotEmptyView.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotEmptyView.kt index 4619762b5..ac7577cee 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotEmptyView.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotEmptyView.kt @@ -13,24 +13,23 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed -import com.acon.acon.core.common.UrlConstants 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.core.ui.android.showToast -import com.acon.acon.feature.spot.mock.spotListUiStateRestaurantMock +import com.acon.acon.core.model.model.spot.Spot +import com.acon.acon.core.model.type.SignInStatus +import com.acon.acon.core.model.type.TransportMode import com.acon.acon.core.ui.compose.LocalRequestSignIn import com.acon.acon.core.ui.compose.getScreenHeight import com.acon.acon.core.ui.compose.toDp +import com.acon.acon.feature.spot.mock.spotListUiStateRestaurantMock import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -38,10 +37,11 @@ private const val MAX_GUEST_AVAILABLE_COUNT = 5 @Composable internal fun SpotEmptyView( - signInStatus: com.acon.acon.core.model.type.SignInStatus, - otherSpots: ImmutableList, - onSpotClick: (com.acon.acon.core.model.model.spot.Spot, rank: Int) -> Unit, - onTryFindWay: (com.acon.acon.core.model.model.spot.Spot) -> Unit, + signInStatus: SignInStatus, + otherSpots: ImmutableList, + onSpotClick: (Spot, rank: Int) -> Unit, + onTryFindWay: (Spot) -> Unit, + onRegisterNewSpotClick: () -> Unit, modifier: Modifier = Modifier, ) { val screenHeightDp = getScreenHeight() @@ -86,7 +86,7 @@ internal fun SpotEmptyView( fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(top = 60.dp, bottom = 24.dp) ) - if (index >= MAX_GUEST_AVAILABLE_COUNT && signInStatus == com.acon.acon.core.model.type.SignInStatus.GUEST) { + if (index >= MAX_GUEST_AVAILABLE_COUNT && signInStatus == SignInStatus.GUEST) { SpotGuestItem( spot = spot, modifier = Modifier @@ -98,7 +98,7 @@ internal fun SpotEmptyView( } else { SpotItem( spot = spot, - transportMode = com.acon.acon.core.model.type.TransportMode.BIKING, + transportMode = TransportMode.BIKING, onItemClick = { onSpotClick(spot, index + 1) }, onFindWayButtonClick = onTryFindWay, modifier = Modifier @@ -109,9 +109,6 @@ internal fun SpotEmptyView( } } } else { - val uriHandler = LocalUriHandler.current - val context = LocalContext.current - Text( text = stringResource(R.string.no_other_spots), style = AconTheme.typography.Title4, @@ -126,11 +123,7 @@ internal fun SpotEmptyView( color = AconTheme.color.Action, fontWeight = FontWeight.SemiBold, modifier = Modifier.noRippleClickable { - try { - uriHandler.openUri(UrlConstants.REQUEST_NEW_SPOT) - } catch (e: Exception) { - context.showToast("웹사이트 접속에 실패했어요") - } + onRegisterNewSpotClick() } ) } @@ -142,10 +135,11 @@ internal fun SpotEmptyView( @Composable private fun SpotListEmptyView1Preview() { SpotEmptyView( - signInStatus = com.acon.acon.core.model.type.SignInStatus.GUEST, + signInStatus = SignInStatus.GUEST, otherSpots = spotListUiStateRestaurantMock.spotList.toImmutableList(), onSpotClick = { _, _ -> }, onTryFindWay = {}, + onRegisterNewSpotClick = {}, modifier = Modifier.fillMaxSize() ) } @@ -154,10 +148,11 @@ private fun SpotListEmptyView1Preview() { @Composable private fun SpotListEmptyView2Preview() { SpotEmptyView( - signInStatus = com.acon.acon.core.model.type.SignInStatus.GUEST, - otherSpots = listOf().toImmutableList(), + signInStatus = SignInStatus.GUEST, + otherSpots = listOf().toImmutableList(), onSpotClick = { _, _ -> }, onTryFindWay = {}, + onRegisterNewSpotClick = {}, modifier = Modifier.fillMaxSize() ) } \ No newline at end of file diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt index 770ef8731..ae569863d 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreen.kt @@ -74,7 +74,8 @@ internal fun SpotListScreen( onNavigateToUploadScreen: () -> Unit = {}, onNavigateToProfileScreen: () -> Unit = {}, onDismissAreaVerificationModalRequest: () -> Unit = {}, - onNavigateToAreaVerificationScreen: () -> Unit = {} + onNavigateToAreaVerificationScreen: () -> Unit = {}, + onRegisterNewSpotClick: () -> Unit = {} ) { val screenHeightDp = getScreenHeight() val screenHeightPx = with(LocalDensity.current) { @@ -196,7 +197,8 @@ internal fun SpotListScreen( itemHeightPx = itemHeightPx, modifier = Modifier.fillMaxSize(), onNavigationAppChoose = onNavigationAppChoose, - onChooseNavigationAppModalDismiss = onChooseNavigationAppModalDismiss + onChooseNavigationAppModalDismiss = onChooseNavigationAppModalDismiss, + onRegisterNewSpotClick = onRegisterNewSpotClick ) } @@ -227,7 +229,8 @@ internal fun SpotListScreen( itemHeightPx = itemHeightPx, modifier = Modifier.fillMaxSize(), onNavigationAppChoose = onNavigationAppChoose, - onChooseNavigationAppModalDismiss = onChooseNavigationAppModalDismiss + onChooseNavigationAppModalDismiss = onChooseNavigationAppModalDismiss, + onRegisterNewSpotClick = onRegisterNewSpotClick ) } } diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreenContainer.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreenContainer.kt index 5ac6b0903..2b8dca673 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreenContainer.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListScreenContainer.kt @@ -32,6 +32,7 @@ fun SpotListScreenContainer( onNavigateToAreaVerificationScreen: (latitude: Double, longitude: Double) -> Unit, modifier: Modifier = Modifier, onNavigateToDeeplinkSpotDetailScreen: (spotNav: SpotNavigationParameter) -> Unit = {}, + onNavigateToUploadPlace: () -> Unit = {}, viewModel: SpotListViewModel = hiltViewModel() ) { val state by viewModel.collectAsState() @@ -63,12 +64,7 @@ fun SpotListScreenContainer( SpotListScreen( state = state, onSpotTypeChanged = viewModel::onSpotTypeClicked, - onSpotClick = { spot, rank -> - if (userType == SignInStatus.GUEST) - onSignInRequired("click_detail_guest?") - else - viewModel.onSpotClicked(spot, rank) - }, + onSpotClick = viewModel::onSpotClicked, onTryFindWay = viewModel::onTryFindWay, onNavigationAppChoose = viewModel::onNavigationAppChosen, onChooseNavigationAppModalDismiss = viewModel::onChooseNavigationAppModalDismissed, @@ -84,6 +80,12 @@ fun SpotListScreenContainer( val lat = (state as? SpotListUiStateV2.Success)?.currentLocation?.latitude ?: 0.0 val lon = (state as? SpotListUiStateV2.Success)?.currentLocation?.longitude ?: 0.0 onNavigateToAreaVerificationScreen(lat, lon) + }, + onRegisterNewSpotClick = { + if (userType == SignInStatus.GUEST) + onSignInRequired("") + else + viewModel.onRegisterNewSpot() } ) } @@ -104,6 +106,10 @@ fun SpotListScreenContainer( is SpotListSideEffectV2.NavigateToExternalMap -> { it.handler.startNavigationApp(context) } + + is SpotListSideEffectV2.NavigateToUploadPlace -> { + onNavigateToUploadPlace() + } } } } \ No newline at end of file diff --git a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListSuccessView.kt b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListSuccessView.kt index 9b8dab509..af349970d 100644 --- a/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListSuccessView.kt +++ b/feature/spot/src/main/java/com/acon/acon/feature/spot/screen/spotlist/composable/SpotListSuccessView.kt @@ -41,17 +41,20 @@ import androidx.compose.ui.zIndex import com.acon.acon.core.designsystem.R import com.acon.acon.core.designsystem.component.bottomsheet.AconBottomSheet import com.acon.acon.core.designsystem.effect.LocalHazeState -import com.acon.acon.core.designsystem.effect.effect.shadowLayerBackground import com.acon.acon.core.designsystem.effect.effect.getOverlayColor +import com.acon.acon.core.designsystem.effect.effect.shadowLayerBackground import com.acon.acon.core.designsystem.noRippleClickable import com.acon.acon.core.designsystem.theme.AconTheme -import com.acon.acon.feature.spot.screen.spotlist.SpotListUiStateV2 -import com.acon.acon.core.ads_api.LocalSpotListAdProvider -import com.acon.acon.core.ui.compose.LocalRequestSignIn -import com.acon.acon.core.ui.compose.toDp +import com.acon.acon.core.model.model.spot.Spot +import com.acon.acon.core.model.type.SignInStatus +import com.acon.acon.core.model.type.TransportMode import com.acon.acon.core.ui.android.KakaoNavigationAppHandler import com.acon.acon.core.ui.android.NaverNavigationAppHandler import com.acon.acon.core.ui.android.NavigationAppHandler +import com.acon.acon.core.ui.compose.LocalRequestSignIn +import com.acon.acon.core.ui.compose.toDp +import com.acon.acon.feature.spot.screen.spotlist.SpotListUiStateV2 +import com.acon.core.ads.SpotListNativeAd import dev.chrisbanes.haze.hazeSource import kotlinx.collections.immutable.toImmutableList import kotlin.math.absoluteValue @@ -63,17 +66,18 @@ private const val MAX_GUEST_AVAILABLE_COUNT = 5 internal fun SpotListSuccessView( pagerState: PagerState, state: SpotListUiStateV2.Success, - signInStatus: com.acon.acon.core.model.type.SignInStatus, - onSpotClick: (com.acon.acon.core.model.model.spot.Spot, rank: Int) -> Unit, - onTryFindWay: (com.acon.acon.core.model.model.spot.Spot) -> Unit, + signInStatus: SignInStatus, + onSpotClick: (Spot, rank: Int) -> Unit, + onTryFindWay: (Spot) -> Unit, itemHeightPx: Float, modifier: Modifier = Modifier, onNavigationAppChoose: (NavigationAppHandler) -> Unit = {}, onChooseNavigationAppModalDismiss: () -> Unit = {}, + onRegisterNewSpotClick: () -> Unit = {} ) { val adInsertedSpot = remember(state.spotList) { - val list: MutableList = state.spotList.toMutableList() + val list: MutableList = state.spotList.toMutableList() if (list.size >= 11) { list.add(11, null) @@ -88,12 +92,13 @@ internal fun SpotListSuccessView( val context = LocalContext.current val onSignInRequired = LocalRequestSignIn.current - if (state.transportMode == com.acon.acon.core.model.type.TransportMode.BIKING) { + if (state.transportMode == TransportMode.BIKING) { SpotEmptyView( signInStatus = signInStatus, otherSpots = state.spotList.toImmutableList(), onSpotClick = onSpotClick, onTryFindWay = onTryFindWay, + onRegisterNewSpotClick = onRegisterNewSpotClick, modifier = modifier .verticalScroll(rememberScrollState()) .hazeSource(LocalHazeState.current) @@ -235,7 +240,7 @@ internal fun SpotListSuccessView( ) } if (spot != null) { - if (page >= MAX_GUEST_AVAILABLE_COUNT && signInStatus == com.acon.acon.core.model.type.SignInStatus.GUEST) { + if (page >= MAX_GUEST_AVAILABLE_COUNT && signInStatus == SignInStatus.GUEST) { SpotGuestItem( spot = spot, onItemClick = { onSignInRequired("click_locked_detail_guest?") }, @@ -264,7 +269,7 @@ internal fun SpotListSuccessView( ) } } else { - LocalSpotListAdProvider.current.NativeAd( + SpotListNativeAd( modifier = Modifier .fillMaxSize() .clip(RoundedCornerShape(20.dp)) diff --git a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/UploadPlaceViewModel.kt b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/UploadPlaceViewModel.kt index e728bdd76..9b1387ddb 100644 --- a/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/UploadPlaceViewModel.kt +++ b/feature/upload/src/main/java/com/acon/acon/feature/upload/screen/UploadPlaceViewModel.kt @@ -9,12 +9,13 @@ import com.acon.acon.core.model.model.upload.Feature import com.acon.acon.core.model.model.upload.SearchedSpotByMap import com.acon.acon.core.model.type.CafeFeatureType import com.acon.acon.core.model.type.CategoryType +import com.acon.acon.core.model.type.ImageType import com.acon.acon.core.model.type.PriceFeatureType import com.acon.acon.core.model.type.RestaurantFeatureType import com.acon.acon.core.model.type.SpotType +import com.acon.acon.domain.repository.AconAppRepository import com.acon.acon.domain.repository.MapSearchRepository import com.acon.acon.domain.repository.UploadRepository -import com.acon.acon.feature.upload.BuildConfig import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -26,23 +27,16 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.ConnectionPool -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.internal.toImmutableList import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.viewmodel.container -import timber.log.Timber -import java.util.concurrent.TimeUnit import javax.inject.Inject @OptIn(FlowPreview::class) @HiltViewModel class UploadPlaceViewModel @Inject constructor( private val mapSearchRepository: MapSearchRepository, + private val aconAppRepository: AconAppRepository, private val uploadRepository: UploadRepository, application: Application ) : AndroidViewModel(application), ContainerHost { @@ -372,9 +366,9 @@ class UploadPlaceViewModel @Inject constructor( val presignedResults = runCatching { coroutineScope { - (0 until uris.size).map { + uris.map { uri -> async(Dispatchers.IO) { - uploadRepository.getUploadPlacePreSignedUrl().getOrThrow() + aconAppRepository.uploadImage(ImageType.SPOT, uri.toString()).getOrThrow() } }.awaitAll() } @@ -383,82 +377,11 @@ class UploadPlaceViewModel @Inject constructor( return@intent }.getOrThrow() - val uploadSuccessful = runCatching { - coroutineScope { - uris.zip(presignedResults).map { (imageUri, presignedResult) -> - async(Dispatchers.IO) { - putPlaceImageToPreSignedUrlOptimized(imageUri, presignedResult.preSignedUrl) - } - }.awaitAll().all { it } - } - }.onFailure { - postSideEffect(UploadPlaceSideEffect.ShowToastUploadImageFailed) - return@intent - }.getOrThrow() - - if (!uploadSuccessful) { - postSideEffect(UploadPlaceSideEffect.ShowToastUploadImageFailed) - return@intent - } - - val bucketUrls = presignedResults.map { "${BuildConfig.BUCKET_URL}${it.fileName}" } + val bucketUrls = presignedResults.map { it } submitUploadPlace(onSuccess = onSuccess, imageList = bucketUrls) } - private suspend fun putPlaceImageToPreSignedUrlOptimized( - imageUri: Uri, - preSignedUrl: String - ): Boolean = withContext(Dispatchers.IO) { - val context = getApplication().applicationContext - - return@withContext try { - val byteArray: ByteArray - val mimeType: String - - if (imageUri.scheme == "content") { - context.contentResolver.openInputStream(imageUri).use { inputStream -> - byteArray = inputStream?.readBytes() - ?: throw IllegalArgumentException("이미지 읽기 실패") - } - mimeType = context.contentResolver.getType(imageUri) ?: "image/jpeg" - } else { - Timber.tag(TAG).e("지원하지 않는 URI scheme: %s", imageUri.toString()) - 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() - - client.newCall(request).execute().use { response -> - if (response.isSuccessful) { - Timber.tag(TAG).d("이미지 업로드 성공") - true - } else { - Timber.tag(TAG).e("이미지 업로드 실패, code: ${response.code}") - false - } - } - } catch (e: Exception) { - Timber.tag(TAG).e(e, "이미지 업로드 과정에서 예외 발생: ${e.message}") - false - } - } - companion object { - private val client: OkHttpClient by lazy { - OkHttpClient.Builder() - .connectionPool(ConnectionPool(20, 5, TimeUnit.MINUTES)) - .connectTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build() - } - const val TAG = "UploadPlaceViewModel" } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e52f58ae1..560124580 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -257,6 +257,8 @@ acon-android-library-haze = { id = "com.acon.android.library.haze", version = "u acon-android-library-coil = { id = "com.acon.android.library.coil", version = "unspecified" } acon-android-library-naver-map = { id = "com.acon.android.library.naver.map", version = "unspecified" } acon-firebase = { id = "com.acon.firebase", version = "unspecified" } +acon-feature-test = { id = "com.acon.feature.test", version = "unspecified"} +acon-common-unit-test = { id = "com.acon.common.unit.test", version = "unspecified"} [bundles] googleSignIn = ["androidx-credentials", "androidx-credentials-play-services-auth", "googleid"] @@ -270,8 +272,10 @@ coil = ["coil-compose", "coil-network-okhttp"] naver-map = ["naver-map-compose", "naver-map-location", "naver-map-sdk"] firebase = ["firebase-analytics-sdk", "firebase-crashlytics-sdk"] play-app-update = ["play-core", "play-core-ktx"] -non-android-test = ["junit4", "kotlin-test", "mockk", "coroutine-test", "turbine", "junit5-api", "junit5-params"] -junit5-runtime = ["junit5-runtime-engine", "junit5-runtime-vintage-engine"] + android-test = ["androidx-junit", "androidx-espresso-core", "androidx-ui-test-junit4"] orbit-test = ["orbit-test"] -kotest = ["kotest-runner", "kotest-property", "kotest-assertions"] \ No newline at end of file +kotest = ["kotest-runner", "kotest-property", "kotest-assertions"] +test-coroutine = ["coroutine-test", "turbine"] +test-unit = ["junit4", "kotlin-test", "junit5-api", "junit5-params"] +test-runtime = ["junit5-runtime-engine", "junit5-runtime-vintage-engine"] diff --git a/provider/ads-impl/.gitignore b/provider/ads-impl/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/provider/ads-impl/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/provider/ads-impl/consumer-rules.pro b/provider/ads-impl/consumer-rules.pro deleted file mode 100644 index e69de29bb..000000000 diff --git a/provider/ads-impl/proguard-rules.pro b/provider/ads-impl/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/provider/ads-impl/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3f8e9319e..6012b2544 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,11 +21,11 @@ dependencyResolutionManagement { } } -rootProject.name = "Acon" +rootProject.name = "ACON-Android" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") includeBuild("build-logic") -include(":app") +include(":acon") include( ":core:designsystem", @@ -44,11 +44,10 @@ include(":domain") include(":feature:profile") include(":feature:settings") include(":core:analytics") -include(":core:ads-api") +include(":core:ads") include(":core:ui") include(":core:model") include(":core:navigation") -include(":provider:ads-impl") include(":core:launcher") include(":core:social") include(":core:data")