diff --git a/.gitignore b/.gitignore index 023aca12..cf54d1e2 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,5 @@ secrets.properties /app/release/* /app/google-services.json + +.kotlin/ diff --git a/README.md b/README.md index b92b6a40..08f1e60b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # unifest-android -[![Kotlin](https://img.shields.io/badge/Kotlin-1.9.23-blue.svg)](https://kotlinlang.org) +[![Kotlin](https://img.shields.io/badge/Kotlin-2.0.0-blue.svg)](https://kotlinlang.org) [![Gradle](https://img.shields.io/badge/gradle-8.7-green.svg)](https://gradle.org/) [![Android Studio](https://img.shields.io/badge/Android%20Studio-2023.2.1%20%28Iguana%29-green)](https://developer.android.com/studio) [![minSdkVersion](https://img.shields.io/badge/minSdkVersion-26-red)](https://developer.android.com/distribute/best-practices/develop/target-sdk) @@ -23,6 +23,7 @@ ## Features ## Article +- [[Android] Type-Safe Compose Navigation 적용 - 중첩(Nested) 네비게이션 구조](https://velog.io/@mraz3068/Android-Type-Safe-Compose-Navigation-Applying-in-Nested-Navigation) - [[Android] Github Action 를 이용하여 CD 를 적용 하는 방법](https://velog.io/@mraz3068/Apply-Android-Github-Action-CD) - [[Android] Jetpack Compose 가로 세로 길이가 같은 반응형 다이얼로그 만들기](https://velog.io/@mraz3068/Creating-Responsive-Dialog-with-Equal-Width-and-Height-in-Jetpack-Compose) - [[Android] Jetpack Compose 에서 Snackbar Duration 을 Custom 하는 방법](https://velog.io/@mraz3068/Android-Compose-Snackbar-Duration-Custom) @@ -35,7 +36,7 @@ - IDE : Android Studio Iguana - JDK : Java 17을 실행할 수 있는 JDK -- Kotlin Language : 1.9.23 +- Kotlin Language : 2.0.0 ### Language diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d0f832d7..a99a5e84 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -64,17 +64,25 @@ dependencies { implementations( projects.core.common, projects.core.data, + projects.core.database, + projects.core.datastore, projects.core.designsystem, + projects.core.model, + projects.core.navigation, projects.core.network, - projects.core.datastore, projects.core.ui, + projects.feature.booth, + projects.feature.festival, projects.feature.home, projects.feature.intro, + projects.feature.likedBooth, projects.feature.main, projects.feature.map, projects.feature.menu, + projects.feature.navigator, projects.feature.splash, + projects.feature.stamp, projects.feature.waiting, libs.androidx.activity.compose, diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 34f878b4..806f17f3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -21,3 +21,97 @@ #-renamesourcefileattribute SourceFile -keep class com.unifest.android.feature.map.model.** { *; } + +-dontwarn com.unifest.android.core.common.ErrorHandlerActions +-dontwarn com.unifest.android.core.common.HandleExceptionKt +-dontwarn com.unifest.android.core.common.ObserveEventKt +-dontwarn com.unifest.android.core.common.PermissionDialogButtonType +-dontwarn com.unifest.android.core.common.UiText$DirectString +-dontwarn com.unifest.android.core.common.UiText +-dontwarn com.unifest.android.core.common.extension.ContextKt +-dontwarn com.unifest.android.core.common.utils.DateUtilsKt +-dontwarn com.unifest.android.core.common.utils.DpToPxKt +-dontwarn com.unifest.android.core.data.datasource.RemoteConfigDataSource +-dontwarn com.unifest.android.core.data.datasource.RemoteConfigDataSourceImpl +-dontwarn com.unifest.android.core.data.di.FirebaseModule_ProvideMessagingFactory +-dontwarn com.unifest.android.core.data.di.FirebaseModule_ProvideRemoteConfigFactory +-dontwarn com.unifest.android.core.data.repository.BoothRepository +-dontwarn com.unifest.android.core.data.repository.BoothRepositoryImpl +-dontwarn com.unifest.android.core.data.repository.FestivalRepository +-dontwarn com.unifest.android.core.data.repository.FestivalRepositoryImpl +-dontwarn com.unifest.android.core.data.repository.LikedBoothRepository +-dontwarn com.unifest.android.core.data.repository.LikedBoothRepositoryImpl +-dontwarn com.unifest.android.core.data.repository.LikedFestivalRepository +-dontwarn com.unifest.android.core.data.repository.LikedFestivalRepositoryImpl +-dontwarn com.unifest.android.core.data.repository.MessagingRepository +-dontwarn com.unifest.android.core.data.repository.MessagingRepositoryImpl +-dontwarn com.unifest.android.core.data.repository.OnboardingRepository +-dontwarn com.unifest.android.core.data.repository.OnboardingRepositoryImpl +-dontwarn com.unifest.android.core.data.repository.RemoteConfigRepository +-dontwarn com.unifest.android.core.data.repository.RemoteConfigRepositoryImpl +-dontwarn com.unifest.android.core.data.repository.WaitingRepository +-dontwarn com.unifest.android.core.data.repository.WaitingRepositoryImpl +-dontwarn com.unifest.android.core.database.LikedBoothDao +-dontwarn com.unifest.android.core.database.LikedFestivalDao +-dontwarn com.unifest.android.core.database.di.DaoModule_ProvideLikedBoothDaoFactory +-dontwarn com.unifest.android.core.database.di.DaoModule_ProvideLikedFestivalDaoFactory +-dontwarn com.unifest.android.core.database.di.DatabaseModule_ProvideLikedBoothDatabaseFactory +-dontwarn com.unifest.android.core.database.di.DatabaseModule_ProvideLikedFestivalDatabaseFactory +-dontwarn com.unifest.android.core.datastore.OnboardingDataSource +-dontwarn com.unifest.android.core.datastore.OnboardingDataSourceImpl +-dontwarn com.unifest.android.core.datastore.RecentLikedFestivalDataSource +-dontwarn com.unifest.android.core.datastore.RecentLikedFestivalDataSourceImpl +-dontwarn com.unifest.android.core.datastore.TokenDataSource +-dontwarn com.unifest.android.core.datastore.TokenDataSourceImpl +-dontwarn com.unifest.android.core.datastore.di.DataStoreModule_ProvideOnboardingDataStore$datastore_releaseFactory +-dontwarn com.unifest.android.core.datastore.di.DataStoreModule_ProvideRecentFestivalDataStore$datastore_releaseFactory +-dontwarn com.unifest.android.core.designsystem.component.ButtonKt +-dontwarn com.unifest.android.core.designsystem.component.DialogKt +-dontwarn com.unifest.android.core.designsystem.component.LoadingWheelKt +-dontwarn com.unifest.android.core.designsystem.component.NetworkImageKt +-dontwarn com.unifest.android.core.designsystem.component.ScaffoldKt +-dontwarn com.unifest.android.core.designsystem.component.SearchTextFieldKt +-dontwarn com.unifest.android.core.designsystem.component.SnackBarKt +-dontwarn com.unifest.android.core.designsystem.theme.ColorKt +-dontwarn com.unifest.android.core.designsystem.theme.FontKt +-dontwarn com.unifest.android.core.designsystem.theme.ThemeKt +-dontwarn com.unifest.android.core.navigation.MainTabRoute$Home +-dontwarn com.unifest.android.core.navigation.MainTabRoute$Map +-dontwarn com.unifest.android.core.navigation.MainTabRoute$Menu +-dontwarn com.unifest.android.core.navigation.MainTabRoute$Stamp +-dontwarn com.unifest.android.core.navigation.MainTabRoute$Waiting +-dontwarn com.unifest.android.core.navigation.MainTabRoute +-dontwarn com.unifest.android.core.navigation.Route +-dontwarn com.unifest.android.core.network.di.ApiModule_ProvideUnifestService$network_releaseFactory +-dontwarn com.unifest.android.core.network.di.NetworkModule_ProvideHttpLoggingInterceptor$network_releaseFactory +-dontwarn com.unifest.android.core.network.di.NetworkModule_ProvideUnifestApiRetrofit$network_releaseFactory +-dontwarn com.unifest.android.core.network.di.NetworkModule_ProvideUnifestOkHttpClient$network_releaseFactory +-dontwarn com.unifest.android.core.network.service.UnifestService +-dontwarn com.unifest.android.core.ui.component.CameraPermissionTextProvider +-dontwarn com.unifest.android.core.ui.component.LikedFestivalGridKt +-dontwarn com.unifest.android.core.ui.component.PermissionDialogKt +-dontwarn com.unifest.android.core.ui.component.PermissionTextProvider +-dontwarn com.unifest.android.feature.booth.navigation.BoothNavigationKt +-dontwarn com.unifest.android.feature.booth.viewmodel.BoothViewModel +-dontwarn com.unifest.android.feature.booth.viewmodel.BoothViewModel_HiltModules$KeyModule +-dontwarn com.unifest.android.feature.festival.viewmodel.FestivalViewModel +-dontwarn com.unifest.android.feature.festival.viewmodel.FestivalViewModel_HiltModules$KeyModule +-dontwarn com.unifest.android.feature.home.navigation.HomeNavigationKt +-dontwarn com.unifest.android.feature.home.viewmodel.HomeViewModel +-dontwarn com.unifest.android.feature.home.viewmodel.HomeViewModel_HiltModules$KeyModule +-dontwarn com.unifest.android.feature.liked_booth.navigation.LikedBoothNavigationKt +-dontwarn com.unifest.android.feature.liked_booth.viewmodel.LikedBoothViewModel +-dontwarn com.unifest.android.feature.liked_booth.viewmodel.LikedBoothViewModel_HiltModules$KeyModule +-dontwarn com.unifest.android.feature.map.navigation.MapNavigationKt +-dontwarn com.unifest.android.feature.map.viewmodel.MapViewModel +-dontwarn com.unifest.android.feature.map.viewmodel.MapViewModel_HiltModules$KeyModule +-dontwarn com.unifest.android.feature.menu.navigation.MenuNavigationKt +-dontwarn com.unifest.android.feature.menu.viewmodel.MenuViewModel +-dontwarn com.unifest.android.feature.menu.viewmodel.MenuViewModel_HiltModules$KeyModule +-dontwarn com.unifest.android.feature.navigator.IntroNavigator +-dontwarn com.unifest.android.feature.navigator.MainNavigator +-dontwarn com.unifest.android.feature.navigator.Navigator$DefaultImpls +-dontwarn com.unifest.android.feature.navigator.Navigator +-dontwarn com.unifest.android.feature.waiting.navigation.WaitingNavigationKt +-dontwarn com.unifest.android.feature.waiting.viewmodel.WaitingViewModel +-dontwarn com.unifest.android.feature.waiting.viewmodel.WaitingViewModel_HiltModules$KeyModule diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e7f766b6..854767d6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,12 @@ + + + + + + + + + + + + diff --git a/app/src/main/kotlin/com/unifest/android/initializer/FirebaseMessagingInitializer.kt b/app/src/main/kotlin/com/unifest/android/initializer/FirebaseMessagingInitializer.kt new file mode 100644 index 00000000..130dbbc3 --- /dev/null +++ b/app/src/main/kotlin/com/unifest/android/initializer/FirebaseMessagingInitializer.kt @@ -0,0 +1,23 @@ +package com.unifest.android.initializer + +import android.content.Context +import androidx.startup.Initializer +import com.google.firebase.messaging.FirebaseMessaging +import timber.log.Timber + +class FirebaseMessagingInitializer : Initializer { + override fun create(context: Context) { + FirebaseMessaging.getInstance().subscribeToTopic("2") + .addOnCompleteListener { task -> + if (task.isSuccessful) { + Timber.d("Subscribed to topic successfully") + } else { + Timber.e("Failed to subscribe to topic") + } + } + } + + override fun dependencies(): List>> { + return emptyList() + } +} diff --git a/app/src/main/kotlin/com/unifest/android/service/UnifestFirebaseMessagingService.kt b/app/src/main/kotlin/com/unifest/android/service/UnifestFirebaseMessagingService.kt new file mode 100644 index 00000000..07f3f3df --- /dev/null +++ b/app/src/main/kotlin/com/unifest/android/service/UnifestFirebaseMessagingService.kt @@ -0,0 +1,75 @@ +package com.unifest.android.service + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.feature.main.MainActivity +import timber.log.Timber + +class UnifestFirebaseMessagingService : FirebaseMessagingService() { + override fun onMessageReceived(remoteMessage: RemoteMessage) { + super.onMessageReceived(remoteMessage) + if (remoteMessage.notification != null) { + sendNotification(remoteMessage) + } + } + + private fun sendNotification(remoteMessage: RemoteMessage) { + val requestCode = System.currentTimeMillis().toInt() + + val intent = Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP).also { + if (remoteMessage.data["boothId"] != null) { + if (remoteMessage.data["waitingId"] != null) { + Timber.tag("UnifestFirebaseMessagingService").d("waitingId: ${remoteMessage.data["waitingId"]}") + putExtra("navigate_to_waiting", true) + putExtra("waitingId", remoteMessage.data["waitingId"]) + } else { + Timber.tag("UnifestFirebaseMessagingService").d("boothId: ${remoteMessage.data["boothId"]}") + putExtra("navigate_to_booth", true) + putExtra("boothId", remoteMessage.data["boothId"]) + } + } + } + } + + val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + val pendingIntent = PendingIntent.getActivity(this, requestCode, intent, pendingIntentFlags) + + val channelId = CHANNEL_ID + + val notificationBuilder = NotificationCompat.Builder(this, channelId) + .setSmallIcon(designR.mipmap.ic_launcher) + .setContentTitle(remoteMessage.notification?.title.toString()) + .setContentText(remoteMessage.notification?.body.toString()) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val channel = NotificationChannel(channelId, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH) + notificationManager.createNotificationChannel(channel) + + notificationManager.notify(requestCode, notificationBuilder.build()) + } + + override fun onNewToken(token: String) { + Timber.d("Refreshed token: $token") + } + + companion object { + private const val CHANNEL_ID = "unifest" + private const val CHANNEL_NAME = "unifest_notification" + } +} diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index dfe28ba0..edac0e96 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -34,8 +34,9 @@ repositories { } java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } } kotlin { @@ -46,6 +47,7 @@ dependencies { compileOnly(libs.gradle.android) compileOnly(libs.gradle.kotlin) compileOnly(libs.gradle.androidx.room) + compileOnly(libs.compose.compiler.extension) compileOnly(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } diff --git a/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt index f22cd8a6..8937b66e 100644 --- a/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt @@ -6,7 +6,7 @@ import org.gradle.kotlin.dsl.configure internal class AndroidApplicationComposeConventionPlugin : BuildLogicConventionPlugin( { - applyPlugins(Plugins.AndroidApplication) + applyPlugins(Plugins.ANDROID_APPLICATION, Plugins.COMPOSE_COMPILER) extensions.configure { configureCompose(this) diff --git a/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 67a8f93e..06bf4ea0 100644 --- a/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -7,7 +7,7 @@ import org.gradle.kotlin.dsl.configure internal class AndroidApplicationConventionPlugin : BuildLogicConventionPlugin( { - applyPlugins(Plugins.AndroidApplication, Plugins.KotlinAndroid) + applyPlugins(Plugins.ANDROID_APPLICATION, Plugins.KOTLIN_ANDROID) extensions.configure { configureAndroid(this) diff --git a/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt index a42a0d76..859aa95a 100644 --- a/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -17,6 +17,7 @@ internal class AndroidFeatureConventionPlugin : BuildLogicConventionPlugin( implementation(project(path = ":core:designsystem")) implementation(project(path = ":core:model")) implementation(project(path = ":core:ui")) + implementation(project(path = ":core:navigation")) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.hilt.navigation.compose) diff --git a/build-logic/src/main/kotlin/AndroidFirebaseConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidFirebaseConventionPlugin.kt index 893f1ac8..c7a1cd96 100644 --- a/build-logic/src/main/kotlin/AndroidFirebaseConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidFirebaseConventionPlugin.kt @@ -6,13 +6,14 @@ import org.gradle.kotlin.dsl.dependencies internal class AndroidFirebaseConventionPlugin : BuildLogicConventionPlugin( { - applyPlugins(Plugins.GoogleServices, Plugins.FirebaseCrashlytics) + applyPlugins(Plugins.GOOGLE_SERVICES, Plugins.FIREBASE_CRASHLYTICS) dependencies { implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) implementation(libs.firebase.config) + implementation(libs.firebase.messaging) } }, ) diff --git a/build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt index a6efc68b..33f750eb 100644 --- a/build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt @@ -7,7 +7,7 @@ import org.gradle.kotlin.dsl.dependencies internal class AndroidHiltConventionPlugin : BuildLogicConventionPlugin( { - applyPlugins(Plugins.hilt, Plugins.Ksp) + applyPlugins(Plugins.HILT, Plugins.KSP) dependencies { implementation(libs.hilt.android) diff --git a/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 3a661bbd..cf0b5b87 100644 --- a/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -6,7 +6,7 @@ import org.gradle.kotlin.dsl.configure internal class AndroidLibraryComposeConventionPlugin : BuildLogicConventionPlugin( { - applyPlugins(Plugins.AndroidLibrary) + applyPlugins(Plugins.ANDROID_LIBRARY, Plugins.COMPOSE_COMPILER) extensions.configure { configureCompose(this) diff --git a/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 4c4f8c30..eaac2499 100644 --- a/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -6,7 +6,7 @@ import com.unifest.android.libs import org.gradle.kotlin.dsl.configure internal class AndroidLibraryConventionPlugin : BuildLogicConventionPlugin({ - applyPlugins(Plugins.AndroidLibrary, Plugins.KotlinAndroid) + applyPlugins(Plugins.ANDROID_LIBRARY, Plugins.KOTLIN_ANDROID) extensions.configure { configureAndroid(this) diff --git a/build-logic/src/main/kotlin/AndroidRetrofitConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidRetrofitConventionPlugin.kt index c78e6c05..a61a022a 100644 --- a/build-logic/src/main/kotlin/AndroidRetrofitConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidRetrofitConventionPlugin.kt @@ -6,7 +6,7 @@ import org.gradle.kotlin.dsl.dependencies internal class AndroidRetrofitConventionPlugin : BuildLogicConventionPlugin( { - applyPlugins(Plugins.KotlinxSerialization) + applyPlugins(Plugins.KOTLINX_SERIALIZATION) dependencies { implementation(libs.retrofit) diff --git a/build-logic/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidRoomConventionPlugin.kt index 5a2eb0e8..2f3426c5 100644 --- a/build-logic/src/main/kotlin/AndroidRoomConventionPlugin.kt +++ b/build-logic/src/main/kotlin/AndroidRoomConventionPlugin.kt @@ -9,7 +9,7 @@ import org.gradle.kotlin.dsl.dependencies class AndroidRoomConventionPlugin : BuildLogicConventionPlugin( { - applyPlugins(Plugins.AndroidxRoom, Plugins.KotlinxSerialization, Plugins.Ksp) + applyPlugins(Plugins.ANDROIDX_ROOM, Plugins.KOTLINX_SERIALIZATION, Plugins.KSP) extensions.configure { // The schemas directory contains a schema file for each version of the Room database. diff --git a/build-logic/src/main/kotlin/JvmKotlinConventionPlugin.kt b/build-logic/src/main/kotlin/JvmKotlinConventionPlugin.kt index 100e4762..28bd68e8 100644 --- a/build-logic/src/main/kotlin/JvmKotlinConventionPlugin.kt +++ b/build-logic/src/main/kotlin/JvmKotlinConventionPlugin.kt @@ -9,7 +9,7 @@ import org.gradle.kotlin.dsl.dependencies import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension internal class JvmKotlinConventionPlugin : BuildLogicConventionPlugin({ - applyPlugins(Plugins.JavaLibrary, Plugins.KotlinJvm) + applyPlugins(Plugins.JAVA_LIBRARY, Plugins.KOTLIN_JVM) extensions.configure { sourceCompatibility = ApplicationConfig.JavaVersion diff --git a/build-logic/src/main/kotlin/com/unifest/android/Android.kt b/build-logic/src/main/kotlin/com/unifest/android/Android.kt index ddd66968..6252fcd7 100644 --- a/build-logic/src/main/kotlin/com/unifest/android/Android.kt +++ b/build-logic/src/main/kotlin/com/unifest/android/Android.kt @@ -7,7 +7,7 @@ import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension -internal fun Project.configureAndroid(extension: CommonExtension<*, *, *, *, *>) { +internal fun Project.configureAndroid(extension: CommonExtension<*, *, *, *, *, *>) { extension.apply { compileSdk = libs.versions.compileSdk.get().toInt() diff --git a/build-logic/src/main/kotlin/com/unifest/android/Compose.kt b/build-logic/src/main/kotlin/com/unifest/android/Compose.kt index 33e26d02..b4587b1d 100644 --- a/build-logic/src/main/kotlin/com/unifest/android/Compose.kt +++ b/build-logic/src/main/kotlin/com/unifest/android/Compose.kt @@ -2,41 +2,25 @@ package com.unifest.android import com.android.build.api.dsl.CommonExtension import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension -internal fun Project.configureCompose(extension: CommonExtension<*, *, *, *, *>) { +internal fun Project.configureCompose(extension: CommonExtension<*, *, *, *, *, *>) { extension.apply { - buildFeatures { - compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get() - } - dependencies { - implementation(libs.androidx.compose.bom) - androidTestImplementation(libs.androidx.compose.bom) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.bundles.androidx.compose) debugImplementation(libs.androidx.compose.ui.tooling) } - } - tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = freeCompilerArgs + listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$rootDir/report/compose-metrics", - ) - freeCompilerArgs = freeCompilerArgs + listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$rootDir/report/compose-reports", - ) + configure { + enableStrongSkippingMode.set(true) + includeSourceInformation.set(true) + + metricsDestination.file("build/composeMetrics") + reportsDestination.file("build/composeReports") + + stabilityConfigurationFile.set(project.rootDir.resolve("stability.config.conf")) } } } diff --git a/build-logic/src/main/kotlin/com/unifest/android/Extensions.kt b/build-logic/src/main/kotlin/com/unifest/android/Extensions.kt index 47934c0c..9eb35d79 100644 --- a/build-logic/src/main/kotlin/com/unifest/android/Extensions.kt +++ b/build-logic/src/main/kotlin/com/unifest/android/Extensions.kt @@ -22,14 +22,14 @@ internal fun Project.applyPlugins(vararg plugins: String) { } internal val Project.isAndroidProject: Boolean - get() = pluginManager.hasPlugin(Plugins.AndroidApplication) || - pluginManager.hasPlugin(Plugins.AndroidLibrary) + get() = pluginManager.hasPlugin(Plugins.ANDROID_APPLICATION) || + pluginManager.hasPlugin(Plugins.ANDROID_LIBRARY) -internal val Project.androidExtensions: CommonExtension<*, *, *, *, *> +internal val Project.androidExtensions: CommonExtension<*, *, *, *, *, *> get() { - return if (pluginManager.hasPlugin(Plugins.AndroidApplication)) { + return if (pluginManager.hasPlugin(Plugins.ANDROID_APPLICATION)) { extensions.getByType() - } else if (pluginManager.hasPlugin(Plugins.AndroidLibrary)) { + } else if (pluginManager.hasPlugin(Plugins.ANDROID_LIBRARY)) { extensions.getByType() } else { throw GradleException("The provided project does not have the Android plugin applied. ($name)") diff --git a/build-logic/src/main/kotlin/com/unifest/android/Plugins.kt b/build-logic/src/main/kotlin/com/unifest/android/Plugins.kt index 0897d90c..27a9de1d 100644 --- a/build-logic/src/main/kotlin/com/unifest/android/Plugins.kt +++ b/build-logic/src/main/kotlin/com/unifest/android/Plugins.kt @@ -1,21 +1,21 @@ package com.unifest.android internal object Plugins { - const val JavaLibrary = "java-library" - const val KotlinJvm = "org.jetbrains.kotlin.jvm" + const val JAVA_LIBRARY = "java-library" - const val KotlinAndroid = "org.jetbrains.kotlin.android" + const val KOTLIN_JVM = "org.jetbrains.kotlin.jvm" + const val KOTLIN_ANDROID = "org.jetbrains.kotlin.android" + const val KOTLINX_SERIALIZATION = "org.jetbrains.kotlin.plugin.serialization" - const val KotlinxSerialization = "org.jetbrains.kotlin.plugin.serialization" + const val ANDROID_APPLICATION = "com.android.application" + const val ANDROID_LIBRARY = "com.android.library" + const val COMPOSE_COMPILER = "org.jetbrains.kotlin.plugin.compose" - const val AndroidApplication = "com.android.application" - const val AndroidLibrary = "com.android.library" + const val ANDROIDX_ROOM = "androidx.room" - const val AndroidxRoom = "androidx.room" + const val HILT = "dagger.hilt.android.plugin" + const val KSP = "com.google.devtools.ksp" - const val hilt = "dagger.hilt.android.plugin" - const val Ksp = "com.google.devtools.ksp" - - const val GoogleServices = "com.google.gms.google-services" - const val FirebaseCrashlytics = "com.google.firebase.crashlytics" + const val GOOGLE_SERVICES = "com.google.gms.google-services" + const val FIREBASE_CRASHLYTICS = "com.google.firebase.crashlytics" } diff --git a/build.gradle.kts b/build.gradle.kts index 05b5205e..7a371da9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,9 +7,11 @@ plugins { alias(libs.plugins.kotlin.ktlint) alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.androidx.room) apply false + alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.google.service) apply false alias(libs.plugins.firebase.crashlytics) apply false diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 1ee4995f..b1875060 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -5,7 +5,7 @@ plugins { alias(libs.plugins.unifest.android.library.compose) alias(libs.plugins.unifest.android.hilt) alias(libs.plugins.unifest.android.retrofit) - id("kotlin-parcelize") + alias(libs.plugins.kotlin.parcelize) } android { diff --git a/core/common/src/main/kotlin/com/unifest/android/core/common/MultipleEventsCutter.kt b/core/common/src/main/kotlin/com/unifest/android/core/common/MultipleEventsCutter.kt index d524ab49..3d4fe594 100644 --- a/core/common/src/main/kotlin/com/unifest/android/core/common/MultipleEventsCutter.kt +++ b/core/common/src/main/kotlin/com/unifest/android/core/common/MultipleEventsCutter.kt @@ -1,12 +1,12 @@ package com.unifest.android.core.common -internal interface MultipleEventsCutter { +interface MultipleEventsCutter { fun processEvent(event: () -> Unit) companion object } -internal fun MultipleEventsCutter.Companion.get(): MultipleEventsCutter = +fun MultipleEventsCutter.Companion.get(): MultipleEventsCutter = MultipleEventsCutterImpl() private class MultipleEventsCutterImpl : MultipleEventsCutter { diff --git a/core/common/src/main/kotlin/com/unifest/android/core/common/PermissionDialogButtonType.kt b/core/common/src/main/kotlin/com/unifest/android/core/common/PermissionDialogButtonType.kt new file mode 100644 index 00000000..a6811db1 --- /dev/null +++ b/core/common/src/main/kotlin/com/unifest/android/core/common/PermissionDialogButtonType.kt @@ -0,0 +1,7 @@ +package com.unifest.android.core.common + +enum class PermissionDialogButtonType { + DISMISS, + NAVIGATE_TO_APP_SETTING, + CONFIRM, +} diff --git a/core/common/src/main/kotlin/com/unifest/android/core/common/extension/Activity.kt b/core/common/src/main/kotlin/com/unifest/android/core/common/extension/Activity.kt index 0839b608..f37bd6b2 100644 --- a/core/common/src/main/kotlin/com/unifest/android/core/common/extension/Activity.kt +++ b/core/common/src/main/kotlin/com/unifest/android/core/common/extension/Activity.kt @@ -24,7 +24,7 @@ inline fun Activity.startActivityWithAnimation( if (withFinish) finish() } -fun Activity.goToAppSettings() { +fun Activity.navigateToAppSetting() { Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", packageName, null), diff --git a/core/common/src/main/kotlin/com/unifest/android/core/common/utils/DpToPx.kt b/core/common/src/main/kotlin/com/unifest/android/core/common/utils/DpToPx.kt new file mode 100644 index 00000000..d86a458e --- /dev/null +++ b/core/common/src/main/kotlin/com/unifest/android/core/common/utils/DpToPx.kt @@ -0,0 +1,10 @@ +package com.unifest.android.core.common.utils + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +@Composable +fun dpToPx(dp: Dp): Float { + return with(LocalDensity.current) { dp.toPx() } +} diff --git a/core/common/src/main/kotlin/com/unifest/android/core/common/utils/ParseAndFormatTime.kt b/core/common/src/main/kotlin/com/unifest/android/core/common/utils/ParseAndFormatTime.kt new file mode 100644 index 00000000..d983f0f5 --- /dev/null +++ b/core/common/src/main/kotlin/com/unifest/android/core/common/utils/ParseAndFormatTime.kt @@ -0,0 +1,23 @@ +package com.unifest.android.core.common.utils + +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeParseException + +// 포매터 정의 +val formatter = DateTimeFormatter.ofPattern("HH:mm") +val parser = DateTimeFormatter.ofPattern("HH:mm:ss") + +// 시간 파싱 및 형식화 함수 +fun parseAndFormatTime(time: String?): Pair { + return if (time.isNullOrBlank() || time == "등록된 정보가 없습니다") { + "등록된 정보가 없습니다" to null + } else { + try { + val localTime = LocalTime.parse(time, parser) + localTime.format(formatter) to localTime + } catch (e: DateTimeParseException) { + "등록된 정보가 없습니다" to null + } + } +} diff --git a/core/common/src/main/kotlin/com/unifest/android/core/common/utils/PhoneNumberVisualTransformation.kt b/core/common/src/main/kotlin/com/unifest/android/core/common/utils/PhoneNumberVisualTransformation.kt new file mode 100644 index 00000000..cbee638d --- /dev/null +++ b/core/common/src/main/kotlin/com/unifest/android/core/common/utils/PhoneNumberVisualTransformation.kt @@ -0,0 +1,40 @@ +package com.unifest.android.core.common.utils + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation + +class PhoneNumberVisualTransformation : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + val trimmed = if (text.text.length >= 11) text.text.substring(0..10) else text.text + val out = StringBuilder() + + for (i in trimmed.indices) { + out.append(trimmed[i]) + if (i == 2 || i == 6) out.append('-') + } + + val phoneNumberOffsetTranslator = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return when { + offset <= 2 -> offset + offset <= 6 -> offset + 1 + offset <= 10 -> offset + 2 + else -> out.length + } + } + + override fun transformedToOriginal(offset: Int): Int { + return when { + offset <= 3 -> offset + offset <= 8 -> offset - 1 + offset <= out.length -> offset - 2 + else -> trimmed.length + } + } + } + + return TransformedText(AnnotatedString(out.toString()), phoneNumberOffsetTranslator) + } +} diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index bb0eff1b..94e437e4 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -3,8 +3,7 @@ plugins { alias(libs.plugins.unifest.android.library) alias(libs.plugins.unifest.android.hilt) - alias(libs.plugins.unifest.android.firebase) - id("kotlinx-serialization") + alias(libs.plugins.kotlin.serialization) } android { @@ -28,6 +27,9 @@ dependencies { projects.core.model, projects.core.network, + platform(libs.firebase.bom), + libs.firebase.config, + libs.firebase.messaging, libs.timber, ) } diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/datasource/RemoteConfigDataSource.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/datasource/RemoteConfigDataSource.kt index 7b881bef..b231597f 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/datasource/RemoteConfigDataSource.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/datasource/RemoteConfigDataSource.kt @@ -5,13 +5,4 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfigValue interface RemoteConfigDataSource { suspend fun getValue(key: String): FirebaseRemoteConfigValue? suspend fun getString(key: String): String? - suspend fun getString(key: String, defaultValue: String): String - suspend fun getLong(key: String): Long? - suspend fun getLong(key: String, defaultValue: Long): Long - - suspend fun getBoolean(key: String): Boolean? - suspend fun getBoolean(key: String, defaultValue: Boolean): Boolean - - suspend fun getDouble(key: String): Double? - suspend fun getDouble(key: String, defaultValue: Double): Double } diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/datasource/RemoteConfigDataSourceImpl.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/datasource/RemoteConfigDataSourceImpl.kt index 9b2eb22f..c7faf761 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/datasource/RemoteConfigDataSourceImpl.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/datasource/RemoteConfigDataSourceImpl.kt @@ -27,14 +27,4 @@ class RemoteConfigDataSourceImpl @Inject constructor( } override suspend fun getString(key: String): String? = getValue(key)?.asString() - override suspend fun getString(key: String, defaultValue: String): String = getValue(key)?.asString() ?: defaultValue - - override suspend fun getLong(key: String): Long? = getValue(key)?.asLong() - override suspend fun getLong(key: String, defaultValue: Long): Long = getValue(key)?.asLong() ?: defaultValue - - override suspend fun getBoolean(key: String): Boolean? = getValue(key)?.asBoolean() - override suspend fun getBoolean(key: String, defaultValue: Boolean): Boolean = getValue(key)?.asBoolean() ?: defaultValue - - override suspend fun getDouble(key: String): Double? = getValue(key)?.asDouble() - override suspend fun getDouble(key: String, defaultValue: Double): Double = getValue(key)?.asDouble() ?: defaultValue } diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/di/FirebaseModule.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/di/FirebaseModule.kt index 57c24bea..ab01e019 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/di/FirebaseModule.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/di/FirebaseModule.kt @@ -1,5 +1,6 @@ package com.unifest.android.core.data.di +import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.remoteConfigSettings import com.unifest.android.core.data.BuildConfig @@ -24,4 +25,10 @@ internal object FirebaseModule { setConfigSettingsAsync(configSettings) } } + + @Singleton + @Provides + fun provideMessaging(): FirebaseMessaging { + return FirebaseMessaging.getInstance() + } } diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/di/RepositoryModule.kt index 771564e0..b77e3eef 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/di/RepositoryModule.kt @@ -8,10 +8,14 @@ import com.unifest.android.core.data.repository.LikedBoothRepositoryImpl import com.unifest.android.core.data.repository.LikedBoothRepository import com.unifest.android.core.data.repository.LikedFestivalRepository import com.unifest.android.core.data.repository.LikedFestivalRepositoryImpl +import com.unifest.android.core.data.repository.MessagingRepository +import com.unifest.android.core.data.repository.MessagingRepositoryImpl import com.unifest.android.core.data.repository.OnboardingRepository import com.unifest.android.core.data.repository.OnboardingRepositoryImpl import com.unifest.android.core.data.repository.RemoteConfigRepository import com.unifest.android.core.data.repository.RemoteConfigRepositoryImpl +import com.unifest.android.core.data.repository.WaitingRepository +import com.unifest.android.core.data.repository.WaitingRepositoryImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -44,4 +48,12 @@ internal abstract class RepositoryModule { @Binds @Singleton abstract fun bindRemoteConfigRepository(remoteConfigRepositoryImpl: RemoteConfigRepositoryImpl): RemoteConfigRepository + + @Binds + @Singleton + abstract fun bingWaitingRepository(waitingRepositoryImpl: WaitingRepositoryImpl): WaitingRepository + + @Binds + @Singleton + abstract fun bindMessagingRepository(messagingRepositoryImpl: MessagingRepositoryImpl): MessagingRepository } diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/BoothEntityMapper.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/BoothEntityMapper.kt index 24fc1a04..f914e368 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/BoothEntityMapper.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/BoothEntityMapper.kt @@ -26,6 +26,7 @@ internal fun MenuEntity.toModel(): MenuModel { name = name, price = price, imgUrl = imgUrl, + status = status, ) } diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/BoothMapper.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/BoothMapper.kt index 59b9a764..e009979f 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/BoothMapper.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/BoothMapper.kt @@ -4,10 +4,12 @@ import com.unifest.android.core.model.BoothDetailModel import com.unifest.android.core.model.BoothModel import com.unifest.android.core.model.LikedBoothModel import com.unifest.android.core.model.MenuModel +import com.unifest.android.core.model.WaitingModel import com.unifest.android.core.network.response.Booth import com.unifest.android.core.network.response.BoothDetail import com.unifest.android.core.network.response.LikedBooth import com.unifest.android.core.network.response.Menu +import com.unifest.android.core.network.response.Waiting internal fun BoothDetail.toModel(): BoothDetailModel { return BoothDetailModel( @@ -21,6 +23,9 @@ internal fun BoothDetail.toModel(): BoothDetailModel { latitude = latitude, longitude = longitude, menus = menus.map { it.toModel() }, + waitingEnabled = waitingEnabled, + openTime = openTime ?: "", + closeTime = closeTime ?: "", ) } @@ -30,6 +35,7 @@ internal fun Menu.toModel(): MenuModel { name = name, price = price, imgUrl = imgUrl ?: "", + status = status ?: "", ) } @@ -59,3 +65,18 @@ internal fun LikedBooth.toModel(): LikedBoothModel { warning = warning, ) } + +internal fun Waiting.toModel(): WaitingModel { + return WaitingModel( + boothId = boothId, + waitingId = waitingId, + partySize = partySize, + tel = tel, + deviceId = deviceId, + createdAt = createdAt, + updatedAt = updatedAt, + status = status, + waitingOrder = waitingOrder ?: 0L, + boothName = boothName, + ) +} diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/MyWaitingMapper.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/MyWaitingMapper.kt new file mode 100644 index 00000000..2bbc2757 --- /dev/null +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/MyWaitingMapper.kt @@ -0,0 +1,19 @@ +package com.unifest.android.core.data.mapper + +import com.unifest.android.core.model.MyWaitingModel +import com.unifest.android.core.network.response.MyWaiting + +internal fun MyWaiting.toModel(): MyWaitingModel { + return MyWaitingModel( + boothId = boothId, + waitingId = waitingId, + partySize = partySize, + tel = tel, + deviceId = deviceId, + createdAt = createdAt, + updatedAt = updatedAt, + status = status, + waitingOrder = waitingOrder, + boothName = boothName, + ) +} diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/BoothRepository.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/BoothRepository.kt index 77a2170b..c1ca6391 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/BoothRepository.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/BoothRepository.kt @@ -2,6 +2,7 @@ package com.unifest.android.core.data.repository import com.unifest.android.core.model.BoothModel import com.unifest.android.core.model.BoothDetailModel +import com.unifest.android.core.model.WaitingModel interface BoothRepository { suspend fun getPopularBooths(festivalId: Long): Result> @@ -9,4 +10,6 @@ interface BoothRepository { suspend fun getBoothDetail(boothId: Long): Result suspend fun likeBooth(boothId: Long): Result suspend fun getBoothLikes(boothId: Long): Result + suspend fun checkPinValidation(boothId: Long, pinNumber: String): Result + suspend fun requestBoothWaiting(boothId: Long, tel: String, partySize: Long, pinNumber: String): Result } diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/BoothRepositoryImpl.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/BoothRepositoryImpl.kt index f06b77bf..0d598980 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/BoothRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/BoothRepositoryImpl.kt @@ -4,6 +4,9 @@ import android.content.Context import com.unifest.android.core.common.getDeviceId import com.unifest.android.core.data.mapper.toModel import com.unifest.android.core.data.util.runSuspendCatching +import com.unifest.android.core.datastore.TokenDataSource +import com.unifest.android.core.network.request.BoothWaitingRequest +import com.unifest.android.core.network.request.CheckPinValidationRequest import com.unifest.android.core.network.request.LikeBoothRequest import com.unifest.android.core.network.service.UnifestService import dagger.hilt.android.qualifiers.ApplicationContext @@ -12,6 +15,7 @@ import javax.inject.Inject class BoothRepositoryImpl @Inject constructor( @ApplicationContext private val context: Context, private val service: UnifestService, + private val tokenDataSource: TokenDataSource, ) : BoothRepository { override suspend fun getPopularBooths(festivalId: Long) = runSuspendCatching { service.getPopularBooths(festivalId).data.map { it.toModel() } @@ -37,4 +41,27 @@ class BoothRepositoryImpl @Inject constructor( override suspend fun getBoothLikes(boothId: Long) = runSuspendCatching { service.getBoothLikes(boothId).data } + + override suspend fun checkPinValidation(boothId: Long, pinNumber: String): Result = runSuspendCatching { + service.checkPinValidation( + CheckPinValidationRequest( + boothId = boothId, + pinNumber = pinNumber, + ), + ).data + } + + override suspend fun requestBoothWaiting(boothId: Long, tel: String, partySize: Long, pinNumber: String) = runSuspendCatching { + val fcmToken = tokenDataSource.getFCMToken() + service.requestBoothWaiting( + BoothWaitingRequest( + boothId = boothId, + tel = tel, + deviceId = getDeviceId(context), + partySize = partySize, + pinNumber = pinNumber, + fcmToken = fcmToken, + ), + ).data.toModel() + } } diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedFestivalRepository.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedFestivalRepository.kt index 5e11bbe4..2ef05bfe 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedFestivalRepository.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedFestivalRepository.kt @@ -10,5 +10,9 @@ interface LikedFestivalRepository { suspend fun insertLikedFestivalAtSearch(festival: FestivalModel) suspend fun deleteLikedFestival(festival: FestivalModel) suspend fun getRecentLikedFestival(): String - suspend fun setRecentLikedFestival(schoolName: String) + suspend fun setRecentLikedFestival(festivalName: String) + suspend fun getRecentLikedFestivalId(): Long + suspend fun setRecentLikedFestivalId(festivalId: Long) + suspend fun registerLikedFestival(): Result + suspend fun unregisterLikedFestival(): Result } diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedFestivalRepositoryImpl.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedFestivalRepositoryImpl.kt index fc30fd61..7d472735 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedFestivalRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedFestivalRepositoryImpl.kt @@ -2,10 +2,14 @@ package com.unifest.android.core.data.repository import com.unifest.android.core.data.mapper.toEntity import com.unifest.android.core.data.mapper.toModel +import com.unifest.android.core.data.util.runSuspendCatching import com.unifest.android.core.database.LikedFestivalDao import com.unifest.android.core.datastore.RecentLikedFestivalDataSource +import com.unifest.android.core.datastore.TokenDataSource import com.unifest.android.core.model.FestivalModel import com.unifest.android.core.model.FestivalTodayModel +import com.unifest.android.core.network.request.LikedFestivalRequest +import com.unifest.android.core.network.service.UnifestService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -13,6 +17,8 @@ import javax.inject.Inject internal class LikedFestivalRepositoryImpl @Inject constructor( private val likedFestivalDao: LikedFestivalDao, private val recentLikedFestivalDataSource: RecentLikedFestivalDataSource, + private val tokenDataSource: TokenDataSource, + private val service: UnifestService, ) : LikedFestivalRepository { override fun getLikedFestivals(): Flow> { return likedFestivalDao.getLikedFestivalList().map { likedFestivals -> @@ -35,10 +41,30 @@ internal class LikedFestivalRepositoryImpl @Inject constructor( } override suspend fun getRecentLikedFestival(): String { - return recentLikedFestivalDataSource.getRecentLikedFestival() + return recentLikedFestivalDataSource.getRecentLikedFestivalName() } - override suspend fun setRecentLikedFestival(schoolName: String) { - recentLikedFestivalDataSource.setRecentLikedFestival(schoolName) + override suspend fun setRecentLikedFestival(festivalName: String) { + recentLikedFestivalDataSource.setRecentLikedFestivalName(festivalName) + } + + override suspend fun getRecentLikedFestivalId(): Long { + return recentLikedFestivalDataSource.getRecentLikedFestivalId() + } + + override suspend fun setRecentLikedFestivalId(festivalId: Long) { + recentLikedFestivalDataSource.setRecentLikedFestivalId(festivalId) + } + + override suspend fun registerLikedFestival() = runSuspendCatching { + val festivalId = recentLikedFestivalDataSource.getRecentLikedFestivalId() + val fcmToken = tokenDataSource.getFCMToken() ?: "" + service.registerLikedFestival(LikedFestivalRequest(festivalId, fcmToken)) + } + + override suspend fun unregisterLikedFestival() = runSuspendCatching { + val festivalId = recentLikedFestivalDataSource.getRecentLikedFestivalId() + val fcmToken = tokenDataSource.getFCMToken() ?: "" + service.unregisterLikedFestival(LikedFestivalRequest(festivalId, fcmToken)) } } diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/MessagingRepository.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/MessagingRepository.kt new file mode 100644 index 00000000..d01bd985 --- /dev/null +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/MessagingRepository.kt @@ -0,0 +1,6 @@ +package com.unifest.android.core.data.repository + +interface MessagingRepository { + suspend fun refreshFCMToken(): String? + suspend fun setFCMToken(token: String) +} diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/MessagingRepositoryImpl.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/MessagingRepositoryImpl.kt new file mode 100644 index 00000000..95864c4a --- /dev/null +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/MessagingRepositoryImpl.kt @@ -0,0 +1,32 @@ +package com.unifest.android.core.data.repository + +import com.google.firebase.messaging.FirebaseMessaging +import com.unifest.android.core.datastore.TokenDataSource +import timber.log.Timber +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class MessagingRepositoryImpl @Inject constructor( + private val firebaseMessaging: FirebaseMessaging, + private val tokenDataSource: TokenDataSource, +) : MessagingRepository { + override suspend fun refreshFCMToken(): String? = suspendCoroutine { continuation -> + firebaseMessaging.token.addOnCompleteListener { task -> + if (task.isSuccessful) { + continuation.resume( + task.result.also { + Timber.d("FCM registration token: $it") + }, + ) + } else { + Timber.e(task.exception) + continuation.resume(null) + } + } + } + + override suspend fun setFCMToken(token: String) { + tokenDataSource.setFCMToken(token) + } +} diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/WaitingRepository.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/WaitingRepository.kt new file mode 100644 index 00000000..bdb46bfe --- /dev/null +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/WaitingRepository.kt @@ -0,0 +1,9 @@ +package com.unifest.android.core.data.repository + +import com.unifest.android.core.model.MyWaitingModel + +interface WaitingRepository { + suspend fun getMyWaitingList(): Result> + suspend fun cancelBoothWaiting(waitingId: Long): Result + suspend fun registerFCMTopic(waitingId: String) +} diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/WaitingRepositoryImpl.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/WaitingRepositoryImpl.kt new file mode 100644 index 00000000..99159352 --- /dev/null +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/WaitingRepositoryImpl.kt @@ -0,0 +1,44 @@ +package com.unifest.android.core.data.repository + +import android.content.Context +import com.google.firebase.messaging.FirebaseMessaging +import com.unifest.android.core.common.getDeviceId +import com.unifest.android.core.data.mapper.toModel +import com.unifest.android.core.data.util.runSuspendCatching +import com.unifest.android.core.network.request.WaitingRequest +import com.unifest.android.core.network.service.UnifestService +import dagger.hilt.android.qualifiers.ApplicationContext +import timber.log.Timber +import javax.inject.Inject + +class WaitingRepositoryImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val service: UnifestService, + private val firebaseMessaging: FirebaseMessaging, +) : WaitingRepository { + override suspend fun getMyWaitingList() = runSuspendCatching { + service.getMyWaitingList( + deviceId = getDeviceId(context), + ).data?.map { it.toModel() } ?: emptyList() + } + + override suspend fun cancelBoothWaiting(waitingId: Long): Result = runSuspendCatching { + service.cancelBoothWaiting( + WaitingRequest( + waitingId = waitingId, + deviceId = getDeviceId(context), + ), + ).data.toModel() + } + + override suspend fun registerFCMTopic(waitingId: String) { + firebaseMessaging.subscribeToTopic("waiting_$waitingId") + .addOnCompleteListener { task -> + if (task.isSuccessful) { + Timber.d("Subscribed to topic successfully") + } else { + Timber.e("Failed to subscribe to topic") + } + } + } +} diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index f9409856..e6eef729 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -4,7 +4,7 @@ plugins { alias(libs.plugins.unifest.android.library) alias(libs.plugins.unifest.android.hilt) alias(libs.plugins.unifest.android.room) - id("kotlinx-serialization") + alias(libs.plugins.kotlin.serialization) } android { diff --git a/core/database/src/main/kotlin/com/unifest/android/core/database/entity/LikedBoothEntity.kt b/core/database/src/main/kotlin/com/unifest/android/core/database/entity/LikedBoothEntity.kt index ecb9a45d..8eeb48b8 100644 --- a/core/database/src/main/kotlin/com/unifest/android/core/database/entity/LikedBoothEntity.kt +++ b/core/database/src/main/kotlin/com/unifest/android/core/database/entity/LikedBoothEntity.kt @@ -39,4 +39,5 @@ data class MenuEntity( val name: String = "", val price: Int = 0, val imgUrl: String = "", + val status: String = "", ) diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index 9feb5adf..2996526d 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -3,7 +3,7 @@ plugins { alias(libs.plugins.unifest.android.library) alias(libs.plugins.unifest.android.hilt) - id("kotlinx-serialization") + alias(libs.plugins.kotlin.serialization) } android { diff --git a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/RecentLikedFestivalDataSource.kt b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/RecentLikedFestivalDataSource.kt index 7f967e18..aa7140cd 100644 --- a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/RecentLikedFestivalDataSource.kt +++ b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/RecentLikedFestivalDataSource.kt @@ -1,6 +1,8 @@ package com.unifest.android.core.datastore interface RecentLikedFestivalDataSource { - suspend fun getRecentLikedFestival(): String - suspend fun setRecentLikedFestival(schoolName: String) + suspend fun getRecentLikedFestivalName(): String + suspend fun setRecentLikedFestivalName(festivalName: String) + suspend fun getRecentLikedFestivalId(): Long + suspend fun setRecentLikedFestivalId(festivalId: Long) } diff --git a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/RecentLikedFestivalDataSourceImpl.kt b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/RecentLikedFestivalDataSourceImpl.kt index f964058c..c2c4de7f 100644 --- a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/RecentLikedFestivalDataSourceImpl.kt +++ b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/RecentLikedFestivalDataSourceImpl.kt @@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import com.unifest.android.core.datastore.di.RecentLikedFestivalDataStore import kotlinx.coroutines.flow.catch @@ -16,15 +17,26 @@ class RecentLikedFestivalDataSourceImpl @Inject constructor( ) : RecentLikedFestivalDataSource { private companion object { private val KEY_RECENT_LIKED_FESTIVAL = stringPreferencesKey("recent_liked_festival") + private val KEY_RECENT_LIKED_FESTIVAL_ID = longPreferencesKey("recent_liked_festival_id") } - override suspend fun getRecentLikedFestival(): String = dataStore.data + override suspend fun getRecentLikedFestivalName(): String = dataStore.data .catch { exception -> if (exception is IOException) emit(emptyPreferences()) else throw exception }.first()[KEY_RECENT_LIKED_FESTIVAL] ?: "" - override suspend fun setRecentLikedFestival(schoolName: String) { - dataStore.edit { preferences -> preferences[KEY_RECENT_LIKED_FESTIVAL] = schoolName } + override suspend fun setRecentLikedFestivalName(festivalName: String) { + dataStore.edit { preferences -> preferences[KEY_RECENT_LIKED_FESTIVAL] = festivalName } + } + + override suspend fun getRecentLikedFestivalId(): Long = dataStore.data + .catch { exception -> + if (exception is IOException) emit(emptyPreferences()) + else throw exception + }.first()[KEY_RECENT_LIKED_FESTIVAL_ID] ?: 0L + + override suspend fun setRecentLikedFestivalId(festivalId: Long) { + dataStore.edit { preferences -> preferences[KEY_RECENT_LIKED_FESTIVAL_ID] = festivalId } } } diff --git a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/TokenDataSource.kt b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/TokenDataSource.kt new file mode 100644 index 00000000..bfe6ac65 --- /dev/null +++ b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/TokenDataSource.kt @@ -0,0 +1,6 @@ +package com.unifest.android.core.datastore + +interface TokenDataSource { + suspend fun getFCMToken(): String + suspend fun setFCMToken(token: String) +} diff --git a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/TokenDataSourceImpl.kt b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/TokenDataSourceImpl.kt new file mode 100644 index 00000000..21b2b2a7 --- /dev/null +++ b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/TokenDataSourceImpl.kt @@ -0,0 +1,30 @@ +package com.unifest.android.core.datastore + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.stringPreferencesKey +import com.unifest.android.core.datastore.di.OnboardingDataStore +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import java.io.IOException +import javax.inject.Inject + +class TokenDataSourceImpl @Inject constructor( + @OnboardingDataStore private val dataStore: DataStore, +) : TokenDataSource { + private companion object { + private val KEY_FCM_TOKEN = stringPreferencesKey("fcm_token") + } + + override suspend fun getFCMToken(): String = dataStore.data + .catch { exception -> + if (exception is IOException) emit(emptyPreferences()) + else throw exception + }.first()[KEY_FCM_TOKEN] ?: "" + + override suspend fun setFCMToken(token: String) { + dataStore.edit { preferences -> preferences[KEY_FCM_TOKEN] = token } + } +} diff --git a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataSourceModule.kt b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataSourceModule.kt index 466d7a4a..bc45ff86 100644 --- a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataSourceModule.kt +++ b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataSourceModule.kt @@ -4,6 +4,8 @@ import com.unifest.android.core.datastore.OnboardingDataSource import com.unifest.android.core.datastore.OnboardingDataSourceImpl import com.unifest.android.core.datastore.RecentLikedFestivalDataSource import com.unifest.android.core.datastore.RecentLikedFestivalDataSourceImpl +import com.unifest.android.core.datastore.TokenDataSource +import com.unifest.android.core.datastore.TokenDataSourceImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -20,4 +22,8 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindRecentLikedFestivalDataSource(recentLikedFestivalDataSourceImpl: RecentLikedFestivalDataSourceImpl): RecentLikedFestivalDataSource + + @Binds + @Singleton + abstract fun bindTokenDataSource(tokenDataSourceImpl: TokenDataSourceImpl): TokenDataSource } diff --git a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataStoreModule.kt b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataStoreModule.kt index 8b3059d7..43248534 100644 --- a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataStoreModule.kt +++ b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataStoreModule.kt @@ -17,6 +17,9 @@ private val Context.onboardingDataStore: DataStore by preferencesDa private const val RECENT_LIKED_FESTIVAL_DATASTORE = "recent_liked_festival_datastore" private val Context.recentLikedFestivalDataStore: DataStore by preferencesDataStore(name = RECENT_LIKED_FESTIVAL_DATASTORE) +private const val TOKEN_DATASTORE = "onboarding_datastore" +private val Context.tokenDataStore: DataStore by preferencesDataStore(name = TOKEN_DATASTORE) + @Module @InstallIn(SingletonComponent::class) internal object DataStoreModule { @@ -30,4 +33,9 @@ internal object DataStoreModule { @Singleton @Provides internal fun provideRecentFestivalDataStore(@ApplicationContext context: Context) = context.recentLikedFestivalDataStore + + @TokenDataStore + @Singleton + @Provides + internal fun provideTokenDataStore(@ApplicationContext context: Context) = context.tokenDataStore } diff --git a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataStoreQualifier.kt b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataStoreQualifier.kt index e9ac9dd7..56a40594 100644 --- a/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataStoreQualifier.kt +++ b/core/datastore/src/main/kotlin/com/unifest/android/core/datastore/di/DataStoreQualifier.kt @@ -9,3 +9,7 @@ annotation class OnboardingDataStore @Qualifier @Retention(AnnotationRetention.BINARY) annotation class RecentLikedFestivalDataStore + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class TokenDataStore diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/ComponentPreview.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/ComponentPreview.kt index b1d48b17..4d2a002b 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/ComponentPreview.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/ComponentPreview.kt @@ -1,6 +1,17 @@ package com.unifest.android.core.designsystem +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.ui.tooling.preview.Preview -@Preview(showBackground = true) +@Preview( + name = "Light", + showBackground = true, + uiMode = UI_MODE_NIGHT_NO, +) +@Preview( + name = "Dark", + showBackground = true, + uiMode = UI_MODE_NIGHT_YES, +) annotation class ComponentPreview diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/BoothFilterChip.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/BoothFilterChip.kt index 2f6c833d..ff59d0fa 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/BoothFilterChip.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/BoothFilterChip.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardColors +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -16,7 +17,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.theme.BoothLocation -import com.unifest.android.core.designsystem.theme.MainColor import com.unifest.android.core.designsystem.theme.UnifestTheme @Composable @@ -30,12 +30,12 @@ fun BoothFilterChip( modifier = modifier.padding(4.dp), shape = RoundedCornerShape(34.dp), colors = CardColors( - containerColor = if (isSelected) Color(0xFFFFF0F3) else Color.White, - contentColor = if (isSelected) MainColor else Color(0xFF4B4B4B), - disabledContainerColor = Color.White, - disabledContentColor = Color(0xFF585858), + containerColor = if (isSelected) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.background, + contentColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + disabledContainerColor = MaterialTheme.colorScheme.background, + disabledContentColor = MaterialTheme.colorScheme.onSecondaryContainer, ), - border = BorderStroke(1.dp, if (isSelected) MainColor else Color(0xFFD2D2D2)), + border = BorderStroke(1.dp, if (isSelected) MaterialTheme.colorScheme.primary else Color(0xFFD2D2D2)), ) { Box( modifier = Modifier.clickable(onClick = { onChipClick(filterName) }), @@ -55,7 +55,7 @@ fun BoothFilterChip( @ComponentPreview @Composable -fun BoothFilterChipPreview() { +private fun BoothFilterChipPreview() { UnifestTheme { BoothFilterChip( filterName = "주점", @@ -64,3 +64,15 @@ fun BoothFilterChipPreview() { ) } } + +@ComponentPreview +@Composable +private fun SelectedBoothFilterChipPreview() { + UnifestTheme { + BoothFilterChip( + filterName = "주점", + onChipClick = {}, + isSelected = true, + ) + } +} diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Button.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Button.kt index 470320ee..db194030 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Button.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Button.kt @@ -5,27 +5,33 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import com.unifest.android.core.common.MultipleEventsCutter +import com.unifest.android.core.common.get import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.theme.UnifestTheme @Composable fun UnifestButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - containerColor: Color = Color(0xFFf5678E), + containerColor: Color = MaterialTheme.colorScheme.primary, contentColor: Color = Color.White, - disabledContainerColor: Color = Color(0xFF9C9C9C), + disabledContainerColor: Color = MaterialTheme.colorScheme.surfaceVariant, disabledContentColor: Color = Color.White, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit, ) { + val multipleEventsCutter = remember { MultipleEventsCutter.get() } Button( - onClick = onClick, + onClick = { multipleEventsCutter.processEvent { onClick() } }, modifier = modifier, enabled = enabled, shape = RoundedCornerShape(10.dp), @@ -42,10 +48,25 @@ fun UnifestButton( @ComponentPreview @Composable -fun UnifestButtonPreview() { - UnifestButton( - onClick = {}, - ) { - Text("Button") +private fun UnifestButtonPreview() { + UnifestTheme { + UnifestButton( + onClick = {}, + ) { + Text("Button") + } + } +} + +@ComponentPreview +@Composable +private fun UnifestDisabledButtonPreview() { + UnifestTheme { + UnifestButton( + onClick = {}, + enabled = false, + ) { + Text("Button") + } } } diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/CircularOutlineButton.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/CircularOutlineButton.kt new file mode 100644 index 00000000..a0b7f5b3 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/CircularOutlineButton.kt @@ -0,0 +1,56 @@ +package com.unifest.android.core.designsystem.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import com.unifest.android.core.common.extension.noRippleClickable +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.theme.UnifestTheme + +@Composable +fun CircularOutlineButton( + icon: ImageVector, + contentDescription: String?, + onClick: () -> Unit, + borderColor: Color = Color(0xFFD2D2D2), + iconTintColor: Color = MaterialTheme.colorScheme.onBackground, +) { + Box( + modifier = Modifier + .size(27.dp) + .border(BorderStroke(1.dp, borderColor), shape = CircleShape) + .noRippleClickable { onClick() }, + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = icon, + contentDescription = contentDescription, + tint = iconTintColor, + modifier = Modifier.size(13.dp), + ) + } +} + +@ComponentPreview +@Composable +private fun CircularOutlineButtonPreview() { + UnifestTheme { + CircularOutlineButton( + icon = Icons.Default.Remove, + contentDescription = "Minus Button", + onClick = {}, + ) + } +} diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Dialog.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Dialog.kt index 10497044..745ec4be 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Dialog.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Dialog.kt @@ -2,6 +2,7 @@ package com.unifest.android.core.designsystem.component import androidx.annotation.StringRes import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -12,6 +13,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -27,7 +29,8 @@ import androidx.compose.ui.window.DialogProperties import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.R import com.unifest.android.core.designsystem.theme.BoothLocation -import com.unifest.android.core.designsystem.theme.MainColor +import com.unifest.android.core.designsystem.theme.DarkGrey400 +import com.unifest.android.core.designsystem.theme.LightGrey200 import com.unifest.android.core.designsystem.theme.Title2 import com.unifest.android.core.designsystem.theme.Title5 import com.unifest.android.core.designsystem.theme.UnifestTheme @@ -56,7 +59,7 @@ fun UnifestDialog( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(10.dp)) - .background(color = Color.White), + .background(color = MaterialTheme.colorScheme.surface), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.height(27.dp)) @@ -71,12 +74,12 @@ fun UnifestDialog( Text( text = stringResource(id = titleResId), style = Title2, - color = Color.Black, + color = MaterialTheme.colorScheme.onBackground, ) Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(id = descriptionResId), - color = Color(0xFF545454), + color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, style = BoothLocation, ) @@ -98,7 +101,7 @@ fun UnifestDialog( Modifier }, ), - containerColor = MainColor, + containerColor = MaterialTheme.colorScheme.primary, contentColor = Color.White, ) { Text( @@ -114,11 +117,11 @@ fun UnifestDialog( .weight(1f) .height(45.dp) .padding(start = 4.dp), - containerColor = Color(0xFFD2D2D2), - contentColor = Color.Black, + containerColor = if (isSystemInDarkTheme()) DarkGrey400 else LightGrey200, ) { Text( text = stringResource(id = cancelTextResId), + color = MaterialTheme.colorScheme.onBackground, style = Title5, ) } @@ -133,34 +136,38 @@ fun UnifestDialog( fun ServerErrorDialog( onRetryClick: () -> Unit, ) { - UnifestDialog( - onDismissRequest = {}, - titleResId = R.string.server_error_title, - iconResId = R.drawable.ic_caution, - iconDescription = "Caution Icon", - descriptionResId = R.string.server_error_description, - confirmTextResId = R.string.retry, - cancelTextResId = null, - onCancelClick = {}, - onConfirmClick = onRetryClick, - ) + UnifestTheme { + UnifestDialog( + onDismissRequest = {}, + titleResId = R.string.server_error_title, + iconResId = R.drawable.ic_caution, + iconDescription = "Caution Icon", + descriptionResId = R.string.server_error_description, + confirmTextResId = R.string.retry, + cancelTextResId = null, + onCancelClick = {}, + onConfirmClick = onRetryClick, + ) + } } @Composable fun NetworkErrorDialog( onRetryClick: () -> Unit, ) { - UnifestDialog( - onDismissRequest = {}, - titleResId = R.string.network_error_title, - iconResId = R.drawable.ic_network, - iconDescription = "Network Error Icon", - descriptionResId = R.string.network_error_description, - confirmTextResId = R.string.retry, - cancelTextResId = null, - onCancelClick = {}, - onConfirmClick = onRetryClick, - ) + UnifestTheme { + UnifestDialog( + onDismissRequest = {}, + titleResId = R.string.network_error_title, + iconResId = R.drawable.ic_network, + iconDescription = "Network Error Icon", + descriptionResId = R.string.network_error_description, + confirmTextResId = R.string.retry, + cancelTextResId = null, + onCancelClick = {}, + onConfirmClick = onRetryClick, + ) + } } @Composable @@ -168,17 +175,19 @@ fun LikedFestivalDeleteDialog( onCancelClick: () -> Unit, onConfirmClick: () -> Unit, ) { - UnifestDialog( - onDismissRequest = {}, - titleResId = R.string.liked_festival_delete_title, - iconResId = R.drawable.ic_caution, - iconDescription = "Caution Icon", - descriptionResId = R.string.liked_festival_delete_description, - confirmTextResId = R.string.confirm, - cancelTextResId = R.string.cancel, - onCancelClick = onCancelClick, - onConfirmClick = onConfirmClick, - ) + UnifestTheme { + UnifestDialog( + onDismissRequest = {}, + titleResId = R.string.liked_festival_delete_title, + iconResId = R.drawable.ic_caution, + iconDescription = "Caution Icon", + descriptionResId = R.string.liked_festival_delete_description, + confirmTextResId = R.string.confirm, + cancelTextResId = R.string.cancel, + onCancelClick = onCancelClick, + onConfirmClick = onConfirmClick, + ) + } } @Composable @@ -190,23 +199,25 @@ fun AppUpdateDialog( dismissOnClickOutside = false, ), ) { - UnifestDialog( - onDismissRequest = onDismissRequest, - titleResId = R.string.app_update_title, - iconResId = R.drawable.ic_caution, - iconDescription = "Caution Icon", - descriptionResId = R.string.app_update_description, - confirmTextResId = R.string.app_update_confirm, - cancelTextResId = null, - onCancelClick = {}, - onConfirmClick = onUpdateClick, - properties = properties, - ) + UnifestTheme { + UnifestDialog( + onDismissRequest = onDismissRequest, + titleResId = R.string.app_update_title, + iconResId = R.drawable.ic_caution, + iconDescription = "Caution Icon", + descriptionResId = R.string.app_update_description, + confirmTextResId = R.string.app_update_confirm, + cancelTextResId = null, + onCancelClick = {}, + onConfirmClick = onUpdateClick, + properties = properties, + ) + } } @ComponentPreview @Composable -fun ServerErrorDialogPreview() { +private fun ServerErrorDialogPreview() { UnifestTheme { ServerErrorDialog(onRetryClick = {}) } @@ -214,7 +225,7 @@ fun ServerErrorDialogPreview() { @ComponentPreview @Composable -fun NetworkErrorDialogPreview() { +private fun NetworkErrorDialogPreview() { UnifestTheme { NetworkErrorDialog(onRetryClick = {}) } @@ -222,7 +233,7 @@ fun NetworkErrorDialogPreview() { @ComponentPreview @Composable -fun LikedFestivalDeleteDialogPreview() { +private fun LikedFestivalDeleteDialogPreview() { UnifestTheme { LikedFestivalDeleteDialog( onCancelClick = {}, @@ -233,7 +244,7 @@ fun LikedFestivalDeleteDialogPreview() { @ComponentPreview @Composable -fun AppUpdateDialogPreview() { +private fun AppUpdateDialogPreview() { UnifestTheme { AppUpdateDialog( onDismissRequest = {}, diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/HorizontalDivider.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/HorizontalDivider.kt deleted file mode 100644 index d302af9e..00000000 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/HorizontalDivider.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.unifest.android.core.designsystem.component - -import androidx.compose.material3.HorizontalDivider -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import com.unifest.android.core.designsystem.ComponentPreview -import com.unifest.android.core.designsystem.theme.UnifestTheme - -@Composable -fun UnifestHorizontalDivider( - modifier: Modifier = Modifier, -) { - HorizontalDivider( - thickness = 8.dp, - color = Color(0xFFF1F3F7), - modifier = modifier, - ) -} - -@ComponentPreview -@Composable -fun UnifestHorizontalDividerPreview() { - UnifestTheme { - UnifestHorizontalDivider() - } -} diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/LoadingWheel.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/LoadingWheel.kt index 1bc22305..b161f197 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/LoadingWheel.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/LoadingWheel.kt @@ -2,12 +2,12 @@ package com.unifest.android.core.designsystem.component import androidx.compose.foundation.layout.Box import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.unifest.android.core.common.extension.noRippleClickable import com.unifest.android.core.designsystem.ComponentPreview -import com.unifest.android.core.designsystem.theme.MainColor import com.unifest.android.core.designsystem.theme.UnifestTheme @Composable @@ -18,13 +18,13 @@ fun LoadingWheel( modifier = modifier.noRippleClickable { }, contentAlignment = Alignment.Center, ) { - CircularProgressIndicator(color = MainColor) + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) } } @ComponentPreview @Composable -fun LoadingWheelPreview() { +private fun LoadingWheelPreview() { UnifestTheme { LoadingWheel() } diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/NetworkImage.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/NetworkImage.kt index f426da76..6f1b0d65 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/NetworkImage.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/NetworkImage.kt @@ -26,7 +26,7 @@ fun NetworkImage( ) { if (LocalInspectionMode.current) { Image( - painter = painterResource(id = R.drawable.ic_item_placeholder), + painter = painterResource(id = R.drawable.item_placeholder), contentDescription = "Example Image Icon", modifier = modifier, ) @@ -49,7 +49,7 @@ fun NetworkImage( @ComponentPreview @Composable -fun NetworkImagePreview() { +private fun NetworkImagePreview() { UnifestTheme { NetworkImage( imgUrl = "", diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/OutlinedButton.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/OutlinedButton.kt index ba7b3ad2..4ddb88c8 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/OutlinedButton.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/OutlinedButton.kt @@ -1,10 +1,12 @@ package com.unifest.android.core.designsystem.component import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -13,13 +15,15 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.theme.UnifestTheme @Composable fun UnifestOutlinedButton( onClick: () -> Unit, modifier: Modifier = Modifier, - borderColor: Color = Color(0xFFf5678E), - contentColor: Color = Color(0xFFf5678E), + borderColor: Color = MaterialTheme.colorScheme.primary, + containerColor: Color = MaterialTheme.colorScheme.background, + contentColor: Color = MaterialTheme.colorScheme.primary, enabled: Boolean = true, cornerRadius: Dp = 10.dp, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, @@ -31,6 +35,7 @@ fun UnifestOutlinedButton( enabled = enabled, shape = RoundedCornerShape(cornerRadius), colors = ButtonDefaults.outlinedButtonColors( + containerColor = containerColor, contentColor = contentColor, ), border = BorderStroke(1.dp, borderColor), @@ -41,10 +46,12 @@ fun UnifestOutlinedButton( @ComponentPreview @Composable -fun UnifestOutlinedButtonPreview() { - UnifestOutlinedButton( - onClick = {}, - ) { - Text("Outlined Button") +private fun UnifestOutlinedButtonPreview() { + UnifestTheme { + UnifestOutlinedButton( + onClick = {}, + ) { + Text("Outlined Button") + } } } diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/SearchTextField.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/SearchTextField.kt index c5b54c9a..3e0f3b92 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/SearchTextField.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/SearchTextField.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -19,6 +20,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -26,6 +28,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource @@ -37,12 +40,13 @@ import androidx.compose.ui.unit.dp import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.R import com.unifest.android.core.designsystem.theme.BoothLocation -import com.unifest.android.core.designsystem.theme.MainColor +import com.unifest.android.core.designsystem.theme.LightPrimary100 +import com.unifest.android.core.designsystem.theme.LightPrimary500 import com.unifest.android.core.designsystem.theme.UnifestTheme val unifestTextSelectionColors = TextSelectionColors( - handleColor = MainColor, - backgroundColor = Color(0xFFFAB3BE), + handleColor = LightPrimary500, + backgroundColor = LightPrimary100, ) @Composable @@ -53,16 +57,21 @@ fun SearchTextField( onSearch: (TextFieldValue) -> Unit, clearSearchText: () -> Unit, modifier: Modifier = Modifier, - backgroundColor: Color = Color.White, + backgroundColor: Color = MaterialTheme.colorScheme.background, + textColor: Color = MaterialTheme.colorScheme.onBackground, cornerShape: RoundedCornerShape = RoundedCornerShape(67.dp), - borderStroke: BorderStroke = BorderStroke(width = 1.dp, color = Color(0xFFBABABA)), + borderStroke: BorderStroke = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.secondaryContainer), ) { val keyboardController = LocalSoftwareKeyboardController.current CompositionLocalProvider(LocalTextSelectionColors provides unifestTextSelectionColors) { BasicTextField( value = searchText, - onValueChange = updateSearchText, + onValueChange = { + if (it.text.length <= 20) { + updateSearchText(it) + } + }, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardActions = KeyboardActions( @@ -71,7 +80,9 @@ fun SearchTextField( keyboardController?.hide() }, ), - textStyle = TextStyle(color = Color.Black), + singleLine = true, + textStyle = TextStyle(color = textColor), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), decorationBox = { innerTextField -> Row( modifier = modifier @@ -87,7 +98,7 @@ fun SearchTextField( if (searchText.text.isEmpty()) { Text( text = stringResource(id = searchTextHintRes), - color = Color(0xFF848484), + color = MaterialTheme.colorScheme.onSecondaryContainer, style = BoothLocation, ) } @@ -98,14 +109,15 @@ fun SearchTextField( Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_search), contentDescription = "Search Icon", - tint = Color.Unspecified, -// modifier = Modifier.clickable { -// onSearch(searchText) -// }, + tint = MaterialTheme.colorScheme.secondaryContainer, ) } else { Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_delete_gray), + imageVector = if (isSystemInDarkTheme()) { + ImageVector.vectorResource(R.drawable.ic_delete_dark) + } else { + ImageVector.vectorResource(R.drawable.ic_delete_light) + }, contentDescription = "Delete Icon", tint = Color.Unspecified, modifier = Modifier @@ -131,9 +143,10 @@ fun FestivalSearchTextField( setEnableSearchMode: (Boolean) -> Unit, isSearchMode: Boolean, modifier: Modifier = Modifier, - backgroundColor: Color = Color.White, + backgroundColor: Color = MaterialTheme.colorScheme.background, + textColor: Color = MaterialTheme.colorScheme.onBackground, cornerShape: RoundedCornerShape = RoundedCornerShape(67.dp), - borderStroke: BorderStroke = BorderStroke(width = 1.dp, color = Color(0xFFBABABA)), + borderStroke: BorderStroke = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.secondaryContainer), ) { LaunchedEffect(key1 = searchText.text) { setEnableSearchMode(searchText.text.isNotEmpty()) @@ -142,10 +155,16 @@ fun FestivalSearchTextField( CompositionLocalProvider(LocalTextSelectionColors provides unifestTextSelectionColors) { BasicTextField( value = searchText, - onValueChange = updateSearchText, + onValueChange = { + if (it.text.length <= 20) { + updateSearchText(it) + } + }, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - textStyle = TextStyle(color = Color.Black), + singleLine = true, + textStyle = TextStyle(color = textColor), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), decorationBox = { innerTextField -> Row( modifier = modifier @@ -161,7 +180,7 @@ fun FestivalSearchTextField( Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_back_dark_gray), contentDescription = "Search Icon", - tint = Color(0xFF767676), + tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.clickable { clearSearchText() }, @@ -172,7 +191,7 @@ fun FestivalSearchTextField( if (searchText.text.isEmpty()) { Text( text = stringResource(id = searchTextHintRes), - color = Color(0xFF848484), + color = MaterialTheme.colorScheme.onSecondaryContainer, style = BoothLocation, ) } @@ -206,12 +225,12 @@ fun FestivalSearchTextField( @ComponentPreview @Composable -fun SearchTextFieldPreview() { +private fun SearchTextFieldPreview() { UnifestTheme { SearchTextField( searchText = TextFieldValue(), updateSearchText = {}, - searchTextHintRes = R.string.intro_search_text_hint, + searchTextHintRes = R.string.search_text_hint, onSearch = {}, clearSearchText = {}, modifier = Modifier @@ -224,12 +243,12 @@ fun SearchTextFieldPreview() { @ComponentPreview @Composable -fun FestivalSearchTextFieldPreview() { +private fun FestivalSearchTextFieldPreview() { UnifestTheme { FestivalSearchTextField( searchText = TextFieldValue("건국대학교"), updateSearchText = {}, - searchTextHintRes = R.string.intro_search_text_hint, + searchTextHintRes = R.string.search_text_hint, onSearch = {}, clearSearchText = {}, setEnableSearchMode = {}, diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Tooltip.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Tooltip.kt index 544efb07..9f52f791 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Tooltip.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/Tooltip.kt @@ -15,23 +15,24 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.skydoves.balloon.ArrowOrientation +import com.skydoves.balloon.ArrowOrientationRules import com.skydoves.balloon.BalloonAnimation import com.skydoves.balloon.BalloonSizeSpec import com.skydoves.balloon.compose.Balloon import com.skydoves.balloon.compose.rememberBalloonBuilder -import com.skydoves.balloon.compose.setBackgroundColor import com.unifest.android.core.common.extension.noRippleClickable import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.R import com.unifest.android.core.designsystem.theme.Content5 -import com.unifest.android.core.designsystem.theme.MainColor import com.unifest.android.core.designsystem.theme.Title1 import com.unifest.android.core.designsystem.theme.UnifestTheme +import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable @@ -43,15 +44,18 @@ fun ToolTip( content: @Composable () -> Unit, ) { val scope = rememberCoroutineScope() + val context = LocalContext.current + val builder = rememberBalloonBuilder { setArrowSize(10) setArrowPosition(arrowPosition) setArrowOrientation(arrowOrientation) + setArrowOrientationRules(ArrowOrientationRules.ALIGN_FIXED) setWidth(BalloonSizeSpec.WRAP) setHeight(BalloonSizeSpec.WRAP) setPadding(9) setCornerRadius(8f) - setBackgroundColor(MainColor) + setBackgroundColor(context.getColor(R.color.tooltip_color)) setBalloonAnimation(BalloonAnimation.FADE) setDismissWhenClicked(true) setDismissWhenTouchOutside(false) @@ -77,6 +81,7 @@ fun ToolTip( content() LaunchedEffect(key1 = Unit) { scope.launch { + delay(1000) balloonWindow.awaitAlignEnd() } } @@ -138,7 +143,7 @@ fun SchoolSearchTitleWithToolTip( @ComponentPreview @Composable -fun LikedFestivalToolTipPreview() { +private fun LikedFestivalToolTipPreview() { UnifestTheme { LikedFestivalToolTip( completeOnboarding = {}, @@ -148,7 +153,7 @@ fun LikedFestivalToolTipPreview() { @ComponentPreview @Composable -fun SchoolSearchTitleWithToolTipPreview() { +private fun SchoolSearchTitleWithToolTipPreview() { UnifestTheme { SchoolSearchTitleWithToolTip( title = "건국대학교", diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/TopAppBar.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/TopAppBar.kt index 24a4d15c..6516d145 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/TopAppBar.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/TopAppBar.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -41,8 +42,8 @@ fun UnifestTopAppBar( titleStyle: TextStyle = Title1, @DrawableRes navigationIconRes: Int = R.drawable.ic_arrow_back_dark_gray, navigationIconContentDescription: String? = null, - containerColor: Color = Color.White, - contentColor: Color = Color.Black, + containerColor: Color = MaterialTheme.colorScheme.background, + contentColor: Color = MaterialTheme.colorScheme.onBackground, onNavigationClick: () -> Unit = {}, onTitleClick: (Boolean) -> Unit = {}, isOnboardingCompleted: Boolean = false, @@ -59,7 +60,7 @@ fun UnifestTopAppBar( Icon( imageVector = imageVector, contentDescription = navigationIconContentDescription, - tint = Color.Unspecified, + tint = MaterialTheme.colorScheme.onBackground, ) } } @@ -136,7 +137,7 @@ fun SchoolSearchTitle( @ComponentPreview @Composable -fun UnifestTopAppBarPreview() { +private fun UnifestTopAppBarPreview() { UnifestTheme { UnifestTopAppBar( navigationType = TopAppBarNavigationType.None, @@ -147,7 +148,7 @@ fun UnifestTopAppBarPreview() { @ComponentPreview @Composable -fun SchoolSearchTitlePreview() { +private fun SchoolSearchTitlePreview() { UnifestTheme { SchoolSearchTitle( title = "건국대학교", @@ -158,7 +159,7 @@ fun SchoolSearchTitlePreview() { @ComponentPreview @Composable -fun UnifestTopAppBarWithBackButtonPreview() { +private fun UnifestTopAppBarWithBackButtonPreview() { UnifestTheme { UnifestTopAppBar( navigationType = TopAppBarNavigationType.Back, diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Color.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Color.kt index 7e0ba840..45ff0ab7 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Color.kt @@ -2,12 +2,50 @@ package com.unifest.android.core.designsystem.theme import androidx.compose.ui.graphics.Color -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) +val LightPrimary900 = Color(0xFF4D101E) +val LightPrimary800 = Color(0xFFBA3146) +val LightPrimary700 = Color(0xFFE73D58) +val LightPrimary500 = Color(0xFFFF748A) +val LightPrimary300 = Color(0xFFF895A5) +val LightPrimary100 = Color(0xFFFBC3CB) +val LightPrimary50 = Color(0xFFFFF0F3) -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) +val DarkPrimary900 = Color(0xFFF1D9DE) +val DarkPrimary800 = Color(0xFFEFA7B1) +val DarkPrimary700 = Color(0xFFEF8192) +val DarkPrimary500 = Color(0xFFF55770) +val DarkPrimary300 = Color(0xFFD22F49) +val DarkPrimary100 = Color(0xFFA93043) +val DarkPrimary50 = Color(0xFF3F0F1A) + +val LightGrey900 = Color(0xFF131316) +val LightGrey800 = Color(0xFF2B2B30) +val LightGrey700 = Color(0xFF45464A) +val LightGrey600 = Color(0xFF727276) +val LightGrey500 = Color(0xFF8D8D93) +val LightGrey400 = Color(0xFFBABABF) +val LightGrey300 = Color(0xFFD4D6DC) +val LightGrey200 = Color(0xFFE3E4EA) +val LightGrey100 = Color(0xFFF1F3F7) + +val DarkGrey900 = Color(0xFFECEFF5) +val DarkGrey800 = Color(0xFFD1D3DC) +val DarkGrey700 = Color(0xFFB6B8C1) +val DarkGrey600 = Color(0xFF9697A2) +val DarkGrey500 = Color(0xFF78787F) +val DarkGrey400 = Color(0xFF5A5A60) +val DarkGrey300 = Color(0xFF343438) +val DarkGrey200 = Color(0xFF212126) +val DarkGrey100 = Color(0xFF131316) + +val LightBlueGreen = Color(0xFF1FC0BA) +val LightRed = Color(0xFFFF5858) +val LightOrange = Color(0xFFFF8A1F) +val LightGreen = Color(0xFF15D055) + +val DarkBlueGreen = Color(0xFF00A8A1) +val DarkRed = Color(0xFFF03939) +val DarkOrange = Color(0xFFE16E05) +val DarkGreen = Color(0xFF02B540) val MainColor = Color(0xFFF5687E) diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt index 93a8a627..8c4f66f4 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt @@ -13,12 +13,19 @@ val pretendardFamily = FontFamily( Font(R.font.pretendard_semi_bold, FontWeight.SemiBold, FontStyle.Normal), Font(R.font.pretendard_medium, FontWeight.Medium, FontStyle.Normal), Font(R.font.pretendard_regular, FontWeight.Normal, FontStyle.Normal), + Font(R.font.pretendard_light, FontWeight.Light, FontStyle.Normal), +) + +val StampCount = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, ) val Title0 = TextStyle( fontFamily = pretendardFamily, fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, + fontSize = 22.sp, ) val Title1 = TextStyle( @@ -50,6 +57,7 @@ val Title5 = TextStyle( fontWeight = FontWeight.SemiBold, fontSize = 13.sp, ) + val BoothTitle0 = TextStyle( fontFamily = pretendardFamily, fontWeight = FontWeight.Bold, @@ -68,6 +76,12 @@ val BoothTitle2 = TextStyle( fontSize = 20.sp, ) +val BoothTitle3 = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, +) + val BoothCaution = TextStyle( fontFamily = pretendardFamily, fontWeight = FontWeight.SemiBold, @@ -157,3 +171,39 @@ val Content9 = TextStyle( fontWeight = FontWeight.Normal, fontSize = 14.sp, ) + +val WaitingNumber = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 45.sp, +) + +val WaitingNumber2 = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, +) + +val WaitingNumber3 = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.Bold, + fontSize = 15.sp, +) + +val WaitingNumber4 = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.Bold, + fontSize = 18.sp, +) + +val WaitingNumber5 = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 30.sp, +) + +val QRDescription = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.Light, + fontSize = 13.sp, +) diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Theme.kt index e0f49a5d..067d5871 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Theme.kt @@ -5,26 +5,101 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, -) +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, + // 앱의 주요 브랜드 색상, 주요 액션 버튼 등에 사용 + primary = LightPrimary500, + // primary 색상 위의 텍스트나 아이콘 색상 + onPrimary = Color.White, + // primary 색상의 더 연한 버전, 백그라운드나 강조 영역에 사용 + primaryContainer = LightPrimary100, + // primaryContainer 위의 텍스트나 아이콘 색상 + onPrimaryContainer = LightPrimary900, + // 보조 액션이나 정보를 위한 색상 + secondary = LightBlueGreen, + // secondary 색상 위의 텍스트나 아이콘 색상 + onSecondary = LightGrey700, + // secondary 색상의 더 연한 버전, 보조 컨테이너에 사용 + secondaryContainer = LightGrey400, + // secondaryContainer 위의 텍스트나 아이콘 색상 + onSecondaryContainer = LightGrey400, + // 대비를 위한 액센트 색상 + tertiary = LightPrimary50, + // tertiary 색상 위의 텍스트나 아이콘 색상 + onTertiary = Color.White, + // tertiary 색상의 더 연한 버전 + tertiaryContainer = Color.White, + // tertiaryContainer 위의 텍스트나 아이콘 색상 + onTertiaryContainer = Color.White, + // 오류 표시를 위한 색상 + error = LightRed, + // error 색상 위의 텍스트나 아이콘 색상 + onError = Color.White, + // error 색상의 더 연한 버전, 오류 메시지 배경 등에 사용 + errorContainer = LightPrimary100, + // errorContainer 위의 텍스트나 아이콘 색상 + onErrorContainer = LightPrimary900, + // 앱의 배경색 + background = Color.White, + // background 위의 텍스트나 아이콘 색상 + onBackground = LightGrey900, + // 카드, 시트 등 표면 요소의 색상 + surface = Color.White, + // surface 위의 텍스트나 아이콘 색상 + onSurface = LightGrey500, + // surface의 변형, 비활성화된 요소나 구분선에 사용 + surfaceVariant = LightGrey600, + surfaceTint = LightRed, + // surfaceVariant 위의 텍스트나 아이콘 색상 + onSurfaceVariant = LightGrey600, + // 경계선이나 구분선 등에 사용되는 색상 + outline = LightGrey200, + scrim = LightGrey300, + surfaceBright = LightGrey100, + surfaceContainer = Color.White, + surfaceContainerHigh = LightPrimary50, +) -// // Other default colors to override -// background = Color(0xFFFFFBFE), -// surface = Color(0xFFFFFBFE), -// onPrimary = Color.White, -// onSecondary = Color.White, -// onTertiary = Color.White, -// onBackground = Color(0xFF1C1B1F), -// onSurface = Color(0xFF1C1B1F), +private val DarkColorScheme = darkColorScheme( + // 다크 모드에서의 주요 브랜드 색상 + primary = DarkPrimary500, + onPrimary = Color.White, + primaryContainer = DarkPrimary700, + onPrimaryContainer = DarkPrimary50, + // 다크 모드에서의 보조 색상 + secondary = DarkBlueGreen, + onSecondary = DarkGrey700, + secondaryContainer = DarkGrey400, + onSecondaryContainer = DarkGrey400, + // 다크 모드에서의 강조 색상 + tertiary = DarkPrimary50, + onTertiary = Color.White, + tertiaryContainer = DarkGrey300, + onTertiaryContainer = DarkGrey300, + // 다크 모드에서의 오류 색상 + error = DarkRed, + onError = Color.White, + errorContainer = DarkPrimary300, + onErrorContainer = DarkPrimary50, + // 다크 모드에서의 배경색 + background = DarkGrey100, + onBackground = DarkGrey900, + // 다크 모드에서의 표면 색상 + surface = DarkGrey200, + onSurface = DarkGrey500, + surfaceVariant = DarkGrey600, + onSurfaceVariant = DarkGrey600, + surfaceTint = DarkRed, + // 다크 모드에서의 경계선이나 구분선 색상 + outline = DarkGrey200, + scrim = DarkGrey300, + surfaceBright = DarkGrey200, + surfaceContainer = DarkGrey100, + surfaceContainerHigh = DarkGrey300, ) @Composable @@ -34,9 +109,13 @@ fun UnifestTheme( ) { val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content, - ) + CompositionLocalProvider( + LocalDensity provides Density(density = LocalDensity.current.density, fontScale = 1f), + ) { + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content, + ) + } } diff --git a/core/designsystem/src/main/res/drawable-night/ic_location_green.xml b/core/designsystem/src/main/res/drawable-night/ic_location_green.xml new file mode 100644 index 00000000..421bbb52 --- /dev/null +++ b/core/designsystem/src/main/res/drawable-night/ic_location_green.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/designsystem/src/main/res/drawable-night/image_placeholder.png b/core/designsystem/src/main/res/drawable-night/image_placeholder.png new file mode 100644 index 00000000..f1b35384 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-night/image_placeholder.png differ diff --git a/core/designsystem/src/main/res/drawable-night/item_placeholder.png b/core/designsystem/src/main/res/drawable-night/item_placeholder.png new file mode 100644 index 00000000..3341cb70 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-night/item_placeholder.png differ diff --git a/core/designsystem/src/main/res/drawable/ic_arrow_below_waiting.xml b/core/designsystem/src/main/res/drawable/ic_arrow_below_waiting.xml new file mode 100644 index 00000000..a7801b1d --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_arrow_below_waiting.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_booth_info.xml b/core/designsystem/src/main/res/drawable/ic_booth_info.xml new file mode 100644 index 00000000..880ed63b --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_booth_info.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_check.xml b/core/designsystem/src/main/res/drawable/ic_check.xml index fd3e27d2..f4b6466d 100644 --- a/core/designsystem/src/main/res/drawable/ic_check.xml +++ b/core/designsystem/src/main/res/drawable/ic_check.xml @@ -1,9 +1,12 @@ + android:width="17dp" + android:height="13dp" + android:viewportWidth="17" + android:viewportHeight="13"> + android:pathData="M1,4.75L7,11L16,1" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="#ffffff" + android:strokeLineCap="round"/> diff --git a/core/designsystem/src/main/res/drawable/ic_checkbox.xml b/core/designsystem/src/main/res/drawable/ic_checkbox.xml new file mode 100644 index 00000000..a4b770b9 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_checkbox.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_checkbox_unchecked.xml b/core/designsystem/src/main/res/drawable/ic_checkbox_unchecked.xml new file mode 100644 index 00000000..4b9a25bc --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_checkbox_unchecked.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_chevron_left.xml b/core/designsystem/src/main/res/drawable/ic_chevron_left.xml deleted file mode 100644 index e6bb3ca9..00000000 --- a/core/designsystem/src/main/res/drawable/ic_chevron_left.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/core/designsystem/src/main/res/drawable/ic_chevron_right.xml b/core/designsystem/src/main/res/drawable/ic_chevron_right.xml deleted file mode 100644 index 24835127..00000000 --- a/core/designsystem/src/main/res/drawable/ic_chevron_right.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/core/designsystem/src/main/res/drawable/ic_cluster_icon.xml b/core/designsystem/src/main/res/drawable/ic_cluster_icon.xml new file mode 100644 index 00000000..4e026e3e --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_cluster_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_delete_dark.xml b/core/designsystem/src/main/res/drawable/ic_delete_dark.xml new file mode 100644 index 00000000..f99fc3a0 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_delete_dark.xml @@ -0,0 +1,16 @@ + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_delete_light.xml b/core/designsystem/src/main/res/drawable/ic_delete_light.xml new file mode 100644 index 00000000..eba2514b --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_delete_light.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/core/designsystem/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_background.xml rename to core/designsystem/src/main/res/drawable/ic_launcher_background.xml diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_foreground.xml rename to core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/core/designsystem/src/main/res/drawable/ic_location_gray.xml b/core/designsystem/src/main/res/drawable/ic_location_black.xml similarity index 94% rename from core/designsystem/src/main/res/drawable/ic_location_gray.xml rename to core/designsystem/src/main/res/drawable/ic_location_black.xml index 102279a3..22777c64 100644 --- a/core/designsystem/src/main/res/drawable/ic_location_gray.xml +++ b/core/designsystem/src/main/res/drawable/ic_location_black.xml @@ -5,7 +5,7 @@ android:viewportHeight="14"> + + diff --git a/core/designsystem/src/main/res/drawable/ic_selected_home.xml b/core/designsystem/src/main/res/drawable/ic_selected_home.xml deleted file mode 100644 index 66e53789..00000000 --- a/core/designsystem/src/main/res/drawable/ic_selected_home.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/core/designsystem/src/main/res/drawable/ic_warning.xml b/core/designsystem/src/main/res/drawable/ic_warning.xml new file mode 100644 index 00000000..f3232164 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_warning.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_image_placeholder.png b/core/designsystem/src/main/res/drawable/image_placeholder.png similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_image_placeholder.png rename to core/designsystem/src/main/res/drawable/image_placeholder.png diff --git a/core/designsystem/src/main/res/drawable/item_divider.xml b/core/designsystem/src/main/res/drawable/item_divider.xml deleted file mode 100644 index 175523ab..00000000 --- a/core/designsystem/src/main/res/drawable/item_divider.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/core/designsystem/src/main/res/drawable/ic_item_placeholder.png b/core/designsystem/src/main/res/drawable/item_placeholder.png similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_item_placeholder.png rename to core/designsystem/src/main/res/drawable/item_placeholder.png diff --git a/core/designsystem/src/main/res/font/pretendard_light.otf b/core/designsystem/src/main/res/font/pretendard_light.otf new file mode 100644 index 00000000..fefa7853 Binary files /dev/null and b/core/designsystem/src/main/res/font/pretendard_light.otf differ diff --git a/core/designsystem/src/main/res/values-night/colors.xml b/core/designsystem/src/main/res/values-night/colors.xml new file mode 100644 index 00000000..a2442641 --- /dev/null +++ b/core/designsystem/src/main/res/values-night/colors.xml @@ -0,0 +1,13 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + + #FFF55770 + #FFF55770 + diff --git a/core/designsystem/src/main/res/values/colors.xml b/core/designsystem/src/main/res/values/colors.xml index c341beac..a02adc71 100644 --- a/core/designsystem/src/main/res/values/colors.xml +++ b/core/designsystem/src/main/res/values/colors.xml @@ -8,8 +8,6 @@ #FF000000 #FFFFFFFF - #FFF5687E - #303F9F - #FCCA3E - #BEBEBE + #FFFF748A + #FFFF748A diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 9aa1936c..9c490aae 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -1,82 +1,17 @@ - - Uni-Fest - 추가 완료 - 관심있는 학교 축제를 추가해보세요 - 관심 학교는 언제든지 수정 가능합니다 - 학교를 검색해보세요 - 나의 관심 축제 - 모두 선택 해제 - - 전체 - 서울 - 경기/인천 - 강원 - 대전/충청 - 광주/전라 - 부산/대구 - 경상도 - - 총 %d개 - 검색 결과가 없어요 - - - 의 축제 일정 - 오늘은 축제가 열리는 학교가 없어요 - 다가오는 축제 일정 - 관심 축제 추가하기 - 관심 축제로 추가 - 관심 축제로 저장했습니다 - 축제 일정 없음 - 오늘은 축제가 열리는 학교가 없어요 + 학교를 검색해보세요 + 여기를 눌러서 학교를 검색해보세요. + 여기를 터치하여 해당 학교로 이동해보세요. + 현재는 한경대학교만 서비스중이에요! + 나의 관심 축제 - - 위치 확인하기 - 메뉴 - 웨이팅 하기 - 웨이팅 기능을 지원하지 않는 부스입니다 관심 부스로 저장했습니다. 관심 부스로 저장을 실패했습니다. 관심 부스에서 삭제했습니다. 관심 부스에서 삭제를 실패했습니다. - 등록된 메뉴가 없어요. - - - 부스 / 주점을 검색해보세요. - 여기를 눌러서 학교를 검색해보세요. - 인기 부스 - %d위 - 권한 필요 - 앱 설정으로 이동 - 확인 - 인기 부스 갱신을 실패했습니다. - 검색 결과가 없습니다 - - - 메뉴 - 나의 관심 축제 - 추가하기 > - 관심 부스 - 더보기 > - 이용 문의 - 운영자 모드 진입 - 관심 부스 없음 - 지도에서 관심있는 부스를 추가해주세요 - 현재는 건국대학교만 서비스중이에요! - - - 관심 부스 - - 학교 / 축제 이름을 검색해보세요. - 나의 관심 축제 - 편집 - 완료 - 추가 - 검색결과 검색 결과가 없어요 - 여기를 터치하여 해당 학교로 이동해보세요. 서버 문제 발생 @@ -92,7 +27,4 @@ 원활한 서비스 이용을 위해\n최신 버전으로 업데이트가 필요합니다 업데이트 하기 - - 이용문의 - diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index 5afa5a46..97e746f3 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -1,6 +1,6 @@ plugins { alias(libs.plugins.unifest.jvm.kotlin) - id("kotlinx-serialization") + alias(libs.plugins.kotlin.serialization) } dependencies { diff --git a/core/model/src/main/kotlin/com/unifest/android/core/model/BoothDetailModel.kt b/core/model/src/main/kotlin/com/unifest/android/core/model/BoothDetailModel.kt index 8f906b9f..81239ad0 100644 --- a/core/model/src/main/kotlin/com/unifest/android/core/model/BoothDetailModel.kt +++ b/core/model/src/main/kotlin/com/unifest/android/core/model/BoothDetailModel.kt @@ -16,6 +16,9 @@ data class BoothDetailModel( val menus: List = emptyList(), val likes: Int = 0, val isLiked: Boolean = false, + val waitingEnabled: Boolean = false, + val openTime: String = "", + val closeTime: String = "", ) @Stable @@ -24,4 +27,19 @@ data class MenuModel( val name: String, val price: Int, val imgUrl: String, + val status: String, +) + +@Stable +data class WaitingModel( + val boothId: Long = 0L, + val waitingId: Long = 0L, + val partySize: Long = 0L, + val tel: String = "", + val deviceId: String = "", + val createdAt: String = "", + val updatedAt: String = "", + val status: String = "", + val waitingOrder: Long = 0L, + val boothName: String = "", ) diff --git a/core/model/src/main/kotlin/com/unifest/android/core/model/MyWaitingModel.kt b/core/model/src/main/kotlin/com/unifest/android/core/model/MyWaitingModel.kt new file mode 100644 index 00000000..33af4f6b --- /dev/null +++ b/core/model/src/main/kotlin/com/unifest/android/core/model/MyWaitingModel.kt @@ -0,0 +1,17 @@ +package com.unifest.android.core.model + +import androidx.compose.runtime.Stable + +@Stable +data class MyWaitingModel( + val boothId: Long = 0L, + val waitingId: Long = 0L, + val partySize: Long = 0L, + val tel: String = "", + val deviceId: String = "", + val createdAt: String = "", + val updatedAt: String = "", + val status: String = "", + val waitingOrder: Long = 0L, + val boothName: String = "", +) diff --git a/core/model/src/main/kotlin/com/unifest/android/core/model/StampBoothModel.kt b/core/model/src/main/kotlin/com/unifest/android/core/model/StampBoothModel.kt new file mode 100644 index 00000000..265a2c37 --- /dev/null +++ b/core/model/src/main/kotlin/com/unifest/android/core/model/StampBoothModel.kt @@ -0,0 +1,16 @@ +package com.unifest.android.core.model + +import androidx.compose.runtime.Stable + +@Stable +data class StampBoothModel( + val id: Long, + val name: String = "", + val category: String = "", + val description: String = "", + val thumbnail: String = "", + val location: String = "", + val latitude: Float = 0F, + val longitude: Float = 0F, + val isChecked: Boolean = false, +) diff --git a/core/navigation/.gitignore b/core/navigation/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/navigation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts new file mode 100644 index 00000000..609f87e6 --- /dev/null +++ b/core/navigation/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.unifest.android.library) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.unifest.android.core.navigation" +} + +dependencies { + implementation(libs.kotlinx.serialization.json) +} diff --git a/core/navigation/src/main/kotlin/com/unifest/android/core/navigation/RouteModel.kt b/core/navigation/src/main/kotlin/com/unifest/android/core/navigation/RouteModel.kt new file mode 100644 index 00000000..a845b0a4 --- /dev/null +++ b/core/navigation/src/main/kotlin/com/unifest/android/core/navigation/RouteModel.kt @@ -0,0 +1,34 @@ +package com.unifest.android.core.navigation + +import kotlinx.serialization.Serializable + +sealed interface MainTabRoute : Route { + @Serializable + data object Home : MainTabRoute + + @Serializable + data object Waiting : MainTabRoute + + @Serializable + data object Map : MainTabRoute + + @Serializable + data object Stamp : MainTabRoute + + @Serializable + data object Menu : MainTabRoute +} + +sealed interface Route { + @Serializable + data object Booth { + @Serializable + data class BoothDetail(val boothId: Long) : Route + + @Serializable + data object BoothLocation : Route + } + + @Serializable + data object LikeBooth : Route +} diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index b1ddaf92..7a266a00 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -9,7 +9,7 @@ plugins { alias(libs.plugins.unifest.android.retrofit) alias(libs.plugins.unifest.android.hilt) alias(libs.plugins.google.secrets) - id("kotlinx-serialization") + alias(libs.plugins.kotlin.serialization) } val localPropertiesFile = rootProject.file("local.properties") @@ -47,5 +47,5 @@ secrets { } fun getServerBaseUrl(propertyKey: String): String { - return gradleLocalProperties(rootDir).getProperty(propertyKey) + return gradleLocalProperties(rootDir, providers).getProperty(propertyKey) } diff --git a/core/network/src/main/kotlin/com/unifest/android/core/network/di/NetworkModule.kt b/core/network/src/main/kotlin/com/unifest/android/core/network/di/NetworkModule.kt index ce2a61ed..ecd91eb3 100644 --- a/core/network/src/main/kotlin/com/unifest/android/core/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/com/unifest/android/core/network/di/NetworkModule.kt @@ -2,7 +2,6 @@ package com.unifest.android.core.network.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.unifest.android.core.network.BuildConfig -import com.unifest.android.core.network.FileApi import com.unifest.android.core.network.UnifestApi import com.unifest.android.core.network.UnifestClient import dagger.Module @@ -28,7 +27,6 @@ private val jsonRule = Json { } private val jsonConverterFactory = jsonRule.asConverterFactory("application/json".toMediaType()) -private val fileConverterFactory = jsonRule.asConverterFactory("multipart/form-data".toMediaType()) @Module @InstallIn(SingletonComponent::class) @@ -74,17 +72,4 @@ internal object NetworkModule { .addConverterFactory(jsonConverterFactory) .build() } - - @FileApi - @Singleton - @Provides - internal fun provideFileApiRetrofit( - @UnifestClient okHttpClient: OkHttpClient, - ): Retrofit { - return Retrofit.Builder() - .baseUrl(BuildConfig.SERVER_BASE_URL) - .client(okHttpClient) - .addConverterFactory(fileConverterFactory) - .build() - } } diff --git a/core/network/src/main/kotlin/com/unifest/android/core/network/request/BoothCreateRequest.kt b/core/network/src/main/kotlin/com/unifest/android/core/network/request/BoothCreateRequest.kt deleted file mode 100644 index e9f4f795..00000000 --- a/core/network/src/main/kotlin/com/unifest/android/core/network/request/BoothCreateRequest.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.unifest.android.core.network.request - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class BoothCreateRequest( - @SerialName("name") - val name: String, - @SerialName("category") - val category: String, - @SerialName("description") - val description: String, - @SerialName("detail") - val detail: String, - @SerialName("thumbnail") - val thumbnail: String, - @SerialName("festivalId") - val festivalId: Long, - @SerialName("latitude") - val latitude: Float, - @SerialName("longitude") - val longitude: Float, - @SerialName("enabled") - val enabled: Boolean, -) diff --git a/core/network/src/main/kotlin/com/unifest/android/core/network/request/BoothWaitingRequest.kt b/core/network/src/main/kotlin/com/unifest/android/core/network/request/BoothWaitingRequest.kt new file mode 100644 index 00000000..37c8f5cc --- /dev/null +++ b/core/network/src/main/kotlin/com/unifest/android/core/network/request/BoothWaitingRequest.kt @@ -0,0 +1,20 @@ +package com.unifest.android.core.network.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class BoothWaitingRequest( + @SerialName("boothId") + val boothId: Long, + @SerialName("tel") + val tel: String, + @SerialName("deviceId") + val deviceId: String, + @SerialName("partySize") + val partySize: Long, + @SerialName("pinNumber") + val pinNumber: String, + @SerialName("fcmToken") + val fcmToken: String, +) diff --git a/core/network/src/main/kotlin/com/unifest/android/core/network/request/CheckPinValidationRequest.kt b/core/network/src/main/kotlin/com/unifest/android/core/network/request/CheckPinValidationRequest.kt new file mode 100644 index 00000000..f5f8b849 --- /dev/null +++ b/core/network/src/main/kotlin/com/unifest/android/core/network/request/CheckPinValidationRequest.kt @@ -0,0 +1,12 @@ +package com.unifest.android.core.network.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CheckPinValidationRequest( + @SerialName("boothId") + val boothId: Long, + @SerialName("pinNumber") + val pinNumber: String, +) diff --git a/core/network/src/main/kotlin/com/unifest/android/core/network/request/LikedFestivalRequest.kt b/core/network/src/main/kotlin/com/unifest/android/core/network/request/LikedFestivalRequest.kt new file mode 100644 index 00000000..ee82a42b --- /dev/null +++ b/core/network/src/main/kotlin/com/unifest/android/core/network/request/LikedFestivalRequest.kt @@ -0,0 +1,12 @@ +package com.unifest.android.core.network.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LikedFestivalRequest( + @SerialName("festivalId") + val festivalId: Long, + @SerialName("fcmToken") + val fcmToken: String, +) diff --git a/core/network/src/main/kotlin/com/unifest/android/core/network/request/WaitingRequest.kt b/core/network/src/main/kotlin/com/unifest/android/core/network/request/WaitingRequest.kt new file mode 100644 index 00000000..2666beae --- /dev/null +++ b/core/network/src/main/kotlin/com/unifest/android/core/network/request/WaitingRequest.kt @@ -0,0 +1,12 @@ +package com.unifest.android.core.network.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class WaitingRequest( + @SerialName("waitingId") + val waitingId: Long, + @SerialName("deviceId") + val deviceId: String, +) diff --git a/core/network/src/main/kotlin/com/unifest/android/core/network/response/BoothResponse.kt b/core/network/src/main/kotlin/com/unifest/android/core/network/response/BoothResponse.kt index c9343f91..397fac71 100644 --- a/core/network/src/main/kotlin/com/unifest/android/core/network/response/BoothResponse.kt +++ b/core/network/src/main/kotlin/com/unifest/android/core/network/response/BoothResponse.kt @@ -65,6 +65,12 @@ data class BoothDetail( val longitude: Float, @SerialName("menus") val menus: List, + @SerialName("waitingEnabled") + val waitingEnabled: Boolean, + @SerialName("openTime") + val openTime: String? = null, + @SerialName("closeTime") + val closeTime: String? = null, ) @Serializable @@ -77,6 +83,8 @@ data class Menu( val price: Int, @SerialName("imgUrl") val imgUrl: String? = null, + @SerialName("menuStatus") + val status: String? = null, ) @Serializable diff --git a/core/network/src/main/kotlin/com/unifest/android/core/network/response/MyWaitingResponse.kt b/core/network/src/main/kotlin/com/unifest/android/core/network/response/MyWaitingResponse.kt new file mode 100644 index 00000000..580ab94c --- /dev/null +++ b/core/network/src/main/kotlin/com/unifest/android/core/network/response/MyWaitingResponse.kt @@ -0,0 +1,38 @@ +package com.unifest.android.core.network.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MyWaitingResponse( + @SerialName("code") + val code: String, + @SerialName("message") + val message: String, + @SerialName("data") + val data: List? = null, +) + +@Serializable +data class MyWaiting( + @SerialName("boothId") + val boothId: Long = 0L, + @SerialName("waitingId") + val waitingId: Long = 0L, + @SerialName("partySize") + val partySize: Long = 0L, + @SerialName("tel") + val tel: String = "", + @SerialName("deviceId") + val deviceId: String = "", + @SerialName("createdAt") + val createdAt: String = "", + @SerialName("updatedAt") + val updatedAt: String = "", + @SerialName("status") + val status: String = "", + @SerialName("waitingOrder") + val waitingOrder: Long = 0L, + @SerialName("boothName") + val boothName: String = "", +) diff --git a/core/network/src/main/kotlin/com/unifest/android/core/network/response/WaitingResponse.kt b/core/network/src/main/kotlin/com/unifest/android/core/network/response/WaitingResponse.kt new file mode 100644 index 00000000..f2096476 --- /dev/null +++ b/core/network/src/main/kotlin/com/unifest/android/core/network/response/WaitingResponse.kt @@ -0,0 +1,48 @@ +package com.unifest.android.core.network.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CheckPinValidationResponse( + @SerialName("code") + val code: String, + @SerialName("message") + val message: String, + @SerialName("data") + val data: Long, +) + +@Serializable +data class WaitingResponse( + @SerialName("code") + val code: String, + @SerialName("message") + val message: String, + @SerialName("data") + val data: Waiting, +) + +@Serializable +data class Waiting( + @SerialName("boothId") + val boothId: Long, + @SerialName("waitingId") + val waitingId: Long, + @SerialName("partySize") + val partySize: Long, + @SerialName("tel") + val tel: String, + @SerialName("deviceId") + val deviceId: String, + @SerialName("createdAt") + val createdAt: String, + @SerialName("updatedAt") + val updatedAt: String, + @SerialName("status") + val status: String, + @SerialName("waitingOrder") + val waitingOrder: Long? = null, + @SerialName("boothName") + val boothName: String, +) diff --git a/core/network/src/main/kotlin/com/unifest/android/core/network/service/UnifestService.kt b/core/network/src/main/kotlin/com/unifest/android/core/network/service/UnifestService.kt index 4bf5b599..270b0129 100644 --- a/core/network/src/main/kotlin/com/unifest/android/core/network/service/UnifestService.kt +++ b/core/network/src/main/kotlin/com/unifest/android/core/network/service/UnifestService.kt @@ -1,68 +1,124 @@ package com.unifest.android.core.network.service +import com.unifest.android.core.network.request.BoothWaitingRequest +import com.unifest.android.core.network.request.CheckPinValidationRequest import com.unifest.android.core.network.request.LikeBoothRequest +import com.unifest.android.core.network.request.LikedFestivalRequest +import com.unifest.android.core.network.request.WaitingRequest import com.unifest.android.core.network.response.AllBoothsResponse import com.unifest.android.core.network.response.BoothDetailResponse +import com.unifest.android.core.network.response.CheckPinValidationResponse import com.unifest.android.core.network.response.FestivalSearchResponse import com.unifest.android.core.network.response.FestivalTodayResponse import com.unifest.android.core.network.response.LikeBoothResponse import com.unifest.android.core.network.response.LikedBoothsResponse +import com.unifest.android.core.network.response.MyWaitingResponse import com.unifest.android.core.network.response.PopularBoothsResponse +import com.unifest.android.core.network.response.WaitingResponse import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.HTTP import retrofit2.http.POST +import retrofit2.http.PUT import retrofit2.http.Path import retrofit2.http.Query interface UnifestService { + // 축제 전체 검색 @GET("festival/all") suspend fun getAllFestivals(): FestivalSearchResponse + // 학교명 검색 @GET("festival") suspend fun searchSchool( @Query("name") name: String, ): FestivalSearchResponse + // 지역별 검색 @GET("festival/region") suspend fun searchRegion( @Query("region") region: String, ): FestivalSearchResponse + // 다가오는 축제 일정 조회 @GET("festival/after") suspend fun getIncomingFestivals(): FestivalSearchResponse + // 오늘의 축제 일정 조회 @GET("festival/today") suspend fun getTodayFestivals( @Query("date") date: String, ): FestivalTodayResponse + // 상위 5개 부스 확인 @GET("api/booths") suspend fun getPopularBooths( @Query("festivalId") festivalId: Long, ): PopularBoothsResponse + // 해당 축제 부스 전체 조회 @GET("api/booths/{festival-id}/booths") suspend fun getAllBooths( @Path("festival-id") festivalId: Long, ): AllBoothsResponse + // 특정 부스 조회 @GET("api/booths/{booth-id}") suspend fun getBoothDetail( @Path("booth-id") boothId: Long, ): BoothDetailResponse + // 좋아요 생성 @POST("api/likes") suspend fun likeBooth( @Body likeBoothRequest: LikeBoothRequest, ): LikeBoothResponse + // 특정 사용자가 좋아요한 부스 목록 조회 @GET("api/likes") suspend fun getLikedBooths( @Query("token") token: String, ): LikedBoothsResponse + // 특정 부스의 좋아요 개수 조회 @GET("api/likes/{booth-id}") suspend fun getBoothLikes( @Path("booth-id") boothId: Long, ): LikeBoothResponse + + // Pin 번호 받기 + @POST("waiting/pin/check") + suspend fun checkPinValidation( + @Body checkPinValidationRequest: CheckPinValidationRequest, + ): CheckPinValidationResponse + + // 웨이팅 추가 + @POST("waiting") + suspend fun requestBoothWaiting( + @Body boothWaitingRequest: BoothWaitingRequest, + ): WaitingResponse + + // 내 예약된 웨이팅 조회(deviceID 기준) + @GET("waiting/me/{deviceId}") + suspend fun getMyWaitingList( + @Path("deviceId") deviceId: String, + ): MyWaitingResponse + + // 사용자 측의 웨이팅 취소 + @PUT("waiting") + suspend fun cancelBoothWaiting( + @Body request: WaitingRequest, + ): WaitingResponse + + // 관심 축제 등록 + @POST("megaphone/subscribe") + suspend fun registerLikedFestival( + @Body likedFestivalRequest: LikedFestivalRequest, + ) + + // 관심 축제 해제 + @HTTP(method = "DELETE", path = "megaphone/subscribe", hasBody = true) + suspend fun unregisterLikedFestival( + @Body likedFestivalRequest: LikedFestivalRequest, + ) } diff --git a/core/ui/src/main/kotlin/com/unifest/android/core/ui/DevicePreview.kt b/core/ui/src/main/kotlin/com/unifest/android/core/ui/DevicePreview.kt index 15e3c970..1f9192fb 100644 --- a/core/ui/src/main/kotlin/com/unifest/android/core/ui/DevicePreview.kt +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/DevicePreview.kt @@ -1,10 +1,19 @@ package com.unifest.android.core.ui +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.ui.tooling.preview.Preview @Preview( - name = "Portrait", + name = "Light", showBackground = true, + uiMode = UI_MODE_NIGHT_NO, + device = "spec:width=360dp,height=800dp,dpi=411", +) +@Preview( + name = "Dark", + showBackground = true, + uiMode = UI_MODE_NIGHT_YES, device = "spec:width=360dp,height=800dp,dpi=411", ) annotation class DevicePreview diff --git a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/BoothFilterChips.kt b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/BoothFilterChips.kt index a13a1840..20220d9f 100644 --- a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/BoothFilterChips.kt +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/BoothFilterChips.kt @@ -1,7 +1,9 @@ package com.unifest.android.core.ui.component +import androidx.compose.foundation.background import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.unifest.android.core.designsystem.ComponentPreview @@ -19,7 +21,7 @@ fun BoothFilterChips( modifier: Modifier = Modifier, ) { LazyRow( - modifier = modifier, + modifier = modifier.background(MaterialTheme.colorScheme.background), ) { items( items = boothFilters, @@ -36,7 +38,7 @@ fun BoothFilterChips( @ComponentPreview @Composable -fun BoothFilterChipsPreview() { +private fun BoothFilterChipsPreview() { UnifestTheme { BoothFilterChips( onChipClick = {}, diff --git a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/EmptyLikedBoothItem.kt b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/EmptyLikedBoothItem.kt index b79e898e..a781c67a 100644 --- a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/EmptyLikedBoothItem.kt +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/EmptyLikedBoothItem.kt @@ -1,5 +1,6 @@ package com.unifest.android.core.ui.component +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -7,24 +8,26 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.ui.R import com.unifest.android.core.designsystem.theme.Content6 import com.unifest.android.core.designsystem.theme.Title2 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.android.core.designsystem.R @Composable fun EmptyLikedBoothItem( modifier: Modifier = Modifier, ) { - Box(modifier = modifier) { + Box( + modifier = modifier.background(MaterialTheme.colorScheme.background), + ) { Column( modifier = Modifier .fillMaxSize() @@ -35,13 +38,13 @@ fun EmptyLikedBoothItem( Text( text = stringResource(id = R.string.menu_liked_booth_empty), style = Title2, - color = Color.Black, + color = MaterialTheme.colorScheme.onBackground, ) Spacer(modifier = Modifier.height(9.dp)) Text( text = stringResource(id = R.string.menu_insert_liked_booth), style = Content6, - color = Color(0xFF848484), + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } @@ -49,7 +52,7 @@ fun EmptyLikedBoothItem( @ComponentPreview @Composable -fun EmptyLikedBoothItemPreview() { +private fun EmptyLikedBoothItemPreview() { UnifestTheme { EmptyLikedBoothItem( modifier = Modifier diff --git a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/LikedBoothItem.kt b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/LikedBoothItem.kt index f386b04c..c9f22c51 100644 --- a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/LikedBoothItem.kt +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/LikedBoothItem.kt @@ -1,9 +1,9 @@ package com.unifest.android.core.ui.component +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -25,11 +26,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.unifest.android.core.common.extension.clickableSingle import com.unifest.android.core.designsystem.ComponentPreview -import com.unifest.android.core.designsystem.R +import com.unifest.android.core.designsystem.R as designR import com.unifest.android.core.designsystem.component.NetworkImage -import com.unifest.android.core.designsystem.theme.MainColor import com.unifest.android.core.designsystem.theme.Title2 import com.unifest.android.core.designsystem.theme.Title5 +import com.unifest.android.core.designsystem.theme.UnifestTheme import com.unifest.android.core.model.LikedBoothModel @Composable @@ -40,21 +41,22 @@ fun LikedBoothItem( deleteLikedBooth: (LikedBoothModel) -> Unit, modifier: Modifier = Modifier, ) { - val bookMarkColor = if (booth.isLiked) MainColor else Color(0xFF4B4B4B) + val bookMarkColor = if (booth.isLiked) MaterialTheme.colorScheme.primary else Color(0xFF4B4B4B) + Column( - modifier = modifier.padding(horizontal = 20.dp), + modifier = modifier + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 20.dp), ) { Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxSize(), - ) { + Row { NetworkImage( imgUrl = booth.thumbnail, contentDescription = "Booth Thumbnail", modifier = Modifier .size(86.dp) .clip(RoundedCornerShape(16.dp)), - placeholder = painterResource(id = R.drawable.ic_item_placeholder), + placeholder = painterResource(id = designR.drawable.item_placeholder), ) Spacer(modifier = Modifier.width(14.dp)) Column( @@ -64,36 +66,37 @@ fun LikedBoothItem( ) { Text( text = booth.name, - style = Title2, + color = MaterialTheme.colorScheme.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, + style = Title2, ) Spacer(modifier = Modifier.height(4.dp)) Text( text = booth.warning, - style = Title5, - color = Color(0xFF545454), + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, + style = Title5, ) Spacer(modifier = Modifier.height(13.dp)) Row { Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_location_green), + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_location_green), contentDescription = "Location Icon", tint = Color.Unspecified, ) Spacer(modifier = Modifier.width(3.dp)) Text( text = booth.location, - style = Title5, - color = Color(0xFF545454), modifier = Modifier.align(Alignment.CenterVertically), + color = MaterialTheme.colorScheme.onSecondary, + style = Title5, ) } } Icon( - imageVector = ImageVector.vectorResource(if (booth.isLiked) R.drawable.ic_bookmarked else R.drawable.ic_bookmark), + imageVector = ImageVector.vectorResource(if (booth.isLiked) designR.drawable.ic_bookmarked else designR.drawable.ic_bookmark), contentDescription = "Bookmark Icon", tint = bookMarkColor, modifier = Modifier @@ -108,7 +111,7 @@ fun LikedBoothItem( Spacer(modifier = Modifier.height(16.dp)) if (index < totalCount - 1) { HorizontalDivider( - color = Color(0xFFDFDFDF), + color = MaterialTheme.colorScheme.outline, thickness = 1.dp, modifier = Modifier.fillMaxWidth(), ) @@ -118,18 +121,20 @@ fun LikedBoothItem( @ComponentPreview @Composable -fun LikedBoothItemPreview() { - LikedBoothItem( - booth = LikedBoothModel( - id = 1, - name = "부스 이름", - category = "부스 카테고리", - description = "부스 설명", - location = "부스 위치", - warning = "학과 전용 부스", - ), - index = 0, - totalCount = 1, - deleteLikedBooth = {}, - ) +private fun LikedBoothItemPreview() { + UnifestTheme { + LikedBoothItem( + booth = LikedBoothModel( + id = 1, + name = "부스 이름", + category = "부스 카테고리", + description = "부스 설명", + location = "부스 위치", + warning = "학과 전용 부스", + ), + index = 0, + totalCount = 1, + deleteLikedBooth = {}, + ) + } } diff --git a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/LikedFestivalGrid.kt b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/LikedFestivalGrid.kt index 64798a45..52c9b8e8 100644 --- a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/LikedFestivalGrid.kt +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/LikedFestivalGrid.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -21,6 +22,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -35,7 +37,7 @@ import androidx.compose.ui.unit.dp import com.unifest.android.core.common.utils.formatToString import com.unifest.android.core.common.utils.toLocalDate import com.unifest.android.core.designsystem.ComponentPreview -import com.unifest.android.core.designsystem.R +import com.unifest.android.core.designsystem.R as designR import com.unifest.android.core.designsystem.component.NetworkImage import com.unifest.android.core.designsystem.theme.Content2 import com.unifest.android.core.designsystem.theme.Content3 @@ -95,12 +97,13 @@ fun FestivalItem( ) { Card( shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = Color.White, contentColor = Color.Black), - border = BorderStroke(1.dp, Color(0xFFD9D9D9)), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.scrim), modifier = modifier, ) { Box( modifier = Modifier + .background(MaterialTheme.colorScheme.background) .clickable { if (isEditMode) { setLikedFestivalDeleteDialogVisible(festival) @@ -122,32 +125,32 @@ fun FestivalItem( modifier = Modifier .size(36.dp) .clip(CircleShape), - placeholder = painterResource(id = R.drawable.ic_item_placeholder), + placeholder = painterResource(id = designR.drawable.item_placeholder), ) Spacer(modifier = Modifier.height(8.dp)) Text( text = festival.schoolName, - color = Color.Black, + color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Center, style = Content2, ) Spacer(modifier = Modifier.height(2.dp)) Text( text = festival.festivalName, - color = Color.Black, + color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Center, style = Content4, ) Spacer(modifier = Modifier.height(2.dp)) Text( "${festival.beginDate.toLocalDate().formatToString()} - ${festival.endDate.toLocalDate().formatToString()}", - color = Color(0xFF979797), + color = MaterialTheme.colorScheme.onSurfaceVariant, style = Content3, ) } if (isEditMode) { Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete_red), + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_delete_red), contentDescription = "Delete Icon", tint = Color.Unspecified, modifier = Modifier @@ -161,7 +164,29 @@ fun FestivalItem( @ComponentPreview @Composable -fun LikedFestivalsGridPreview() { +private fun FestivalItemPreview() { + UnifestTheme { + FestivalItem( + festival = FestivalModel( + 1, + 1, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + onFestivalSelected = {}, + ) + } +} + +@ComponentPreview +@Composable +private fun LikedFestivalsGridPreview() { val selectedFestivals = persistentListOf() repeat(5) { selectedFestivals.add( @@ -190,7 +215,7 @@ fun LikedFestivalsGridPreview() { @ComponentPreview @Composable -fun LikedFestivalsGridEditModePreview() { +private fun LikedFestivalsGridEditModePreview() { val selectedFestivals = persistentListOf() repeat(5) { selectedFestivals.add( diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/PermissionDialog.kt b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/PermissionDialog.kt similarity index 65% rename from feature/map/src/main/kotlin/com/unifest/android/feature/map/PermissionDialog.kt rename to core/ui/src/main/kotlin/com/unifest/android/core/ui/component/PermissionDialog.kt index dce6ba30..bed110d3 100644 --- a/feature/map/src/main/kotlin/com/unifest/android/feature/map/PermissionDialog.kt +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/PermissionDialog.kt @@ -1,4 +1,4 @@ -package com.unifest.android.feature.map +package com.unifest.android.core.ui.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -10,43 +10,45 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.unifest.android.core.common.extension.noRippleClickable import com.unifest.android.core.designsystem.ComponentPreview -import com.unifest.android.core.designsystem.R import com.unifest.android.core.designsystem.theme.Content2 import com.unifest.android.core.designsystem.theme.Title3 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.android.feature.map.viewmodel.MapUiAction -import com.unifest.android.feature.map.viewmodel.PermissionDialogButtonType +import com.unifest.android.core.ui.R +import com.unifest.android.core.designsystem.R as designR @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun PermissionDialog( +fun PermissionDialog( permissionTextProvider: PermissionTextProvider, isPermanentlyDeclined: Boolean, - onMapUiAction: (MapUiAction) -> Unit, + onDismiss: () -> Unit, + navigateToAppSetting: () -> Unit, + onConfirm: () -> Unit, modifier: Modifier = Modifier, ) { BasicAlertDialog( - onDismissRequest = { onMapUiAction(MapUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.DISMISS)) }, + onDismissRequest = onDismiss, content = { Column( modifier = Modifier .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surface) .padding(24.dp), ) { Text( text = stringResource(id = R.string.permission_required), style = Title3, - color = Color(0xFF121212), + color = MaterialTheme.colorScheme.onBackground, ) Spacer(modifier = Modifier.height(8.dp)) Text( @@ -56,35 +58,35 @@ internal fun PermissionDialog( modifier = Modifier .fillMaxWidth() .padding(bottom = 16.dp), - color = Color(0xFF747479), + color = MaterialTheme.colorScheme.onSurfaceVariant, style = Content2, ) - HorizontalDivider(color = Color(0xFFE3E5E9)) + HorizontalDivider(color = MaterialTheme.colorScheme.outline) Text( text = if (isPermanentlyDeclined) { stringResource(id = R.string.go_to_app_setting) } else { - stringResource(id = R.string.check) + stringResource(id = designR.string.confirm) }, modifier = Modifier .fillMaxWidth() .padding(top = 16.dp) .noRippleClickable { if (isPermanentlyDeclined) { - onMapUiAction(MapUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.GO_TO_APP_SETTINGS)) + navigateToAppSetting() } else { - onMapUiAction(MapUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.CONFIRM)) + onConfirm() } }, textAlign = TextAlign.Center, style = Title3, - color = Color(0xFF121212), + color = MaterialTheme.colorScheme.onBackground, ) } }, modifier = modifier .clip(RoundedCornerShape(12.dp)) - .background(color = Color.White), + .background(color = MaterialTheme.colorScheme.background), ) } @@ -102,6 +104,26 @@ class LocationPermissionTextProvider : PermissionTextProvider { } } +class NotificationPermissionTextProvider : PermissionTextProvider { + override fun getDescription(isPermanentlyDeclined: Boolean): String { + return if (isPermanentlyDeclined) { + "알림 권한 요청을 거부하였습니다.\n앱 설정으로 이동하여 권한을 부여할 수 있습니다." + } else { + "관심 축제의 부스 정보를 알림을 통해 제공받으려면 알림 권한이 필요합니다." + } + } +} + +class CameraPermissionTextProvider : PermissionTextProvider { + override fun getDescription(isPermanentlyDeclined: Boolean): String { + return if (isPermanentlyDeclined) { + "카메라 권한 요청을 거부하였습니다.\n앱 설정으로 이동하여 권한을 부여할 수 있습니다." + } else { + "QR 스캔을 하기 위해서는 카메라 권한이 필요합니다." + } + } +} + @ComponentPreview @Composable fun PermissionDialogPreview() { @@ -109,7 +131,9 @@ fun PermissionDialogPreview() { PermissionDialog( permissionTextProvider = LocationPermissionTextProvider(), isPermanentlyDeclined = false, - onMapUiAction = {}, + onDismiss = {}, + navigateToAppSetting = {}, + onConfirm = {}, ) } } diff --git a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/StarImage.kt b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/StarImage.kt index 682b96a4..d2a831bc 100644 --- a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/StarImage.kt +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/StarImage.kt @@ -61,7 +61,7 @@ fun StarImage( @ComponentPreview @Composable -fun StarImagePreview() { +private fun StarImagePreview() { UnifestTheme { StarImage( imgUrl = "", @@ -75,7 +75,7 @@ fun StarImagePreview() { @ComponentPreview @Composable -fun StarImageClickedPreview() { +private fun StarImageClickedPreview() { UnifestTheme { StarImage( imgUrl = "", diff --git a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/WaitingDialog.kt b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/WaitingDialog.kt new file mode 100644 index 00000000..6a465797 --- /dev/null +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/WaitingDialog.kt @@ -0,0 +1,603 @@ +package com.unifest.android.core.ui.component + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import com.unifest.android.core.common.utils.PhoneNumberVisualTransformation +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.component.CircularOutlineButton +import com.unifest.android.core.designsystem.component.UnifestButton +import com.unifest.android.core.designsystem.component.UnifestDialog +import com.unifest.android.core.designsystem.theme.BoothLocation +import com.unifest.android.core.designsystem.theme.BoothTitle2 +import com.unifest.android.core.designsystem.theme.BoothTitle3 +import com.unifest.android.core.designsystem.theme.Content2 +import com.unifest.android.core.designsystem.theme.Content6 +import com.unifest.android.core.designsystem.theme.Title1 +import com.unifest.android.core.designsystem.theme.Title3 +import com.unifest.android.core.designsystem.theme.Title4 +import com.unifest.android.core.designsystem.theme.Title5 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.designsystem.theme.WaitingNumber3 +import com.unifest.android.core.designsystem.theme.WaitingTeam +import com.unifest.android.core.ui.R +import com.unifest.android.core.designsystem.R as designR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WaitingPinDialog( + boothName: String, + pinNumber: String, + isWrongPinInserted: Boolean, + onDismissRequest: () -> Unit, + onPinNumberUpdated: (String) -> Unit, + onDialogPinButtonClick: () -> Unit, +) { + BasicAlertDialog( + onDismissRequest = onDismissRequest, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .background(color = MaterialTheme.colorScheme.surface), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(14.dp)) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_location_green), + contentDescription = "location icon", + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(2.dp)) + Text( + text = boothName, + color = MaterialTheme.colorScheme.onBackground, + style = Title3, + ) + } + Spacer(modifier = Modifier.height(18.dp)) + Text( + text = stringResource(id = R.string.waiting_dialog_enter_booth_pin), + color = MaterialTheme.colorScheme.onBackground, + style = WaitingTeam, + ) + Spacer(modifier = Modifier.height(30.dp)) + Column { + BasicTextField( + value = pinNumber, + onValueChange = { input -> + if (input.matches(Regex("^\\d{0,4}\$"))) { + onPinNumberUpdated(input) + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .border(1.dp, MaterialTheme.colorScheme.onSecondaryContainer, RoundedCornerShape(5.dp)) + .background( + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(5.dp), + ) + .padding(horizontal = 10.dp, vertical = 19.dp), + textStyle = BoothTitle2.copy( + color = MaterialTheme.colorScheme.onBackground, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + decorationBox = { innerTextField -> + if (pinNumber.isEmpty()) { + Text( + text = stringResource(id = R.string.waiting_dialog_enter_booth_pin_hint), + color = MaterialTheme.colorScheme.onSecondaryContainer, + style = BoothTitle2, + ) + } + innerTextField() + }, + ) + Spacer(modifier = Modifier.height(10.dp)) + AnimatedContent(targetState = isWrongPinInserted) { isWrongPinInserted -> + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(14.dp)) + Icon( + imageVector = ImageVector.vectorResource( + id = if (isWrongPinInserted) designR.drawable.ic_warning else designR.drawable.ic_booth_info, + ), + contentDescription = if (isWrongPinInserted) "warning icon" else "booth info icon", + tint = if (isWrongPinInserted) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = if (isWrongPinInserted) { + stringResource(id = R.string.waiting_dialog_enter_booth_pin_description_error) + } else { + stringResource(id = R.string.waiting_dialog_enter_booth_pin_description) + }, + color = if (isWrongPinInserted) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onBackground, + style = Content6, + ) + } + } + } + Spacer(modifier = Modifier.height(35.dp)) + UnifestButton( + onClick = onDialogPinButtonClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Text( + text = stringResource(id = R.string.waiting_dialog_enter_booth_pin_button), + style = Title4, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun WaitingDialog( + boothName: String, + waitingCount: Long, + phoneNumber: String, + partySize: Long, + isPrivacyClicked: Boolean, + onDismissRequest: () -> Unit, + onWaitingMinusClick: () -> Unit, + onWaitingPlusClick: () -> Unit, + onWaitingTelUpdated: (String) -> Unit, + onDialogWaitingButtonClick: () -> Unit, + onPolicyCheckBoxClick: () -> Unit, + onPrivacyPolicyClick: () -> Unit, + onThirdPartyPolicyClick: () -> Unit, +) { + BasicAlertDialog( + onDismissRequest = onDismissRequest, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .background(color = MaterialTheme.colorScheme.surface), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(14.dp)) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_location_green), + contentDescription = "location icon", + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(2.dp)) + Text( + text = boothName, + color = MaterialTheme.colorScheme.onBackground, + style = Title3, + ) + } + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = stringResource(id = R.string.waiting_dialog_telephone_waiting_number), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = BoothLocation, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(id = R.string.waiting_dialog_telephone_waiting_team, waitingCount), + color = MaterialTheme.colorScheme.onBackground, + style = WaitingTeam, + ) + Spacer(modifier = Modifier.height(9.dp)) + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outline, + ) + Spacer(modifier = Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Text( + text = stringResource(id = R.string.waiting_dialog_telephone_people), + color = MaterialTheme.colorScheme.onBackground, + style = Title5, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + CircularOutlineButton( + icon = Icons.Default.Remove, + contentDescription = "Minus Button", + onClick = onWaitingMinusClick, + ) + Spacer(modifier = Modifier.width(20.dp)) + Text( + text = partySize.toString(), + color = MaterialTheme.colorScheme.onBackground, + style = WaitingNumber3, + ) + Spacer(modifier = Modifier.width(20.dp)) + CircularOutlineButton( + icon = Icons.Default.Add, + contentDescription = "Plus Button", + onClick = onWaitingPlusClick, + ) + } + } + Spacer(modifier = Modifier.height(14.dp)) + BasicTextField( + value = phoneNumber, + onValueChange = { input -> + if (input.matches(Regex("^\\d{0,11}\$"))) { + onWaitingTelUpdated(input) + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .border(1.dp, MaterialTheme.colorScheme.onSecondaryContainer, RoundedCornerShape(5.dp)) + .background( + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(5.dp), + ) + .padding(11.dp), + textStyle = Title4.copy( + color = MaterialTheme.colorScheme.onBackground, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + ), + decorationBox = { innerTextField -> + if (phoneNumber.isEmpty()) { + Text( + text = stringResource(id = R.string.waiting_dialog_telephone_waiting_number_hint), + color = MaterialTheme.colorScheme.onSecondary, + style = BoothLocation, + ) + } + innerTextField() + }, + visualTransformation = PhoneNumberVisualTransformation(), + ) + Spacer(modifier = Modifier.height(12.dp)) + Column( + horizontalAlignment = Alignment.Start, + modifier = Modifier.padding(horizontal = 16.dp), + ) { + FlowRow( + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth(), + ) { + Icon( + imageVector = ImageVector.vectorResource( + id = if (isPrivacyClicked) designR.drawable.ic_checkbox else designR.drawable.ic_checkbox_unchecked, + ), + contentDescription = "check box icon", + tint = Color.Unspecified, + modifier = Modifier.clickable { + onPolicyCheckBoxClick() + }, + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(id = R.string.waiting_dialog_telephone_privacy), + color = MaterialTheme.colorScheme.onBackground, + style = Content6.copy( + textDecoration = TextDecoration.Underline, + ), + modifier = Modifier.clickable { + onPrivacyPolicyClick() + }, + ) + Text( + text = " " + stringResource(id = R.string.waiting_dialog_telephone_and) + " ", + color = MaterialTheme.colorScheme.onSecondaryContainer, + style = Content6, + ) + Text( + text = stringResource(id = R.string.waiting_dialog_telephone_third), + color = MaterialTheme.colorScheme.onBackground, + style = Content6.copy( + textDecoration = TextDecoration.Underline, + ), + modifier = Modifier.clickable { + onThirdPartyPolicyClick() + }, + ) + Text( + text = stringResource(id = R.string.waiting_dialog_telephone_agree), + color = MaterialTheme.colorScheme.onSecondaryContainer, + style = Content6, + ) + } + } + Spacer(modifier = Modifier.height(11.dp)) + UnifestButton( + onClick = onDialogWaitingButtonClick, + containerColor = if (isPrivacyClicked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, + enabled = isPrivacyClicked, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Text( + text = stringResource(id = R.string.waiting_dialog_telephone_button), + style = Title4, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WaitingConfirmDialog( + boothName: String, + waitingId: Long, + waitingPartySize: Long, + waitingTeamNumber: Long, + onConfirmClick: () -> Unit, +) { + BasicAlertDialog( + onDismissRequest = {}, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .background(color = MaterialTheme.colorScheme.surface), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(28.dp)) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_location_green), + contentDescription = "location icon", + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(2.dp)) + Text( + text = boothName, + color = MaterialTheme.colorScheme.onBackground, + style = Title3, + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = stringResource(id = R.string.waiting_dialog_waiting_confirm_complete), + color = MaterialTheme.colorScheme.onBackground, + style = Title1, + ) + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = stringResource(id = R.string.waiting_dialog_waiting_confirm_description), + style = Content2, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(12.dp)) + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outline, + ) + Spacer(modifier = Modifier.height(22.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = stringResource(id = R.string.waiting_dialog_waiting_confirm_waiting_number), + color = MaterialTheme.colorScheme.onBackground, + style = Title5, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.waiting_dialog_waiting_confirm_id, waitingId), + color = MaterialTheme.colorScheme.onBackground, + style = BoothTitle3, + ) + } + Spacer(modifier = Modifier.width(25.dp)) + Box( + modifier = Modifier + .size(3.dp) + .background( + color = MaterialTheme.colorScheme.onBackground, + shape = CircleShape, + ), + ) + Spacer(modifier = Modifier.width(25.dp)) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = stringResource(id = R.string.waiting_dialog_waiting_confirm_people), + color = MaterialTheme.colorScheme.onBackground, + style = Title5, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.waiting_dialog_waiting_confirm_people_number, waitingPartySize), + color = MaterialTheme.colorScheme.onBackground, + style = BoothTitle3, + ) + } + Spacer(modifier = Modifier.width(25.dp)) + Box( + modifier = Modifier + .size(3.dp) + .background( + color = MaterialTheme.colorScheme.onBackground, + shape = CircleShape, + ), + ) + Spacer(modifier = Modifier.width(25.dp)) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = stringResource(id = R.string.waiting_dialog_waiting_confirm_previous_waiting), + color = MaterialTheme.colorScheme.onBackground, + style = Title5, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.waiting_dialog_waiting_confirm_previous_waiting_number, waitingTeamNumber), + color = MaterialTheme.colorScheme.onBackground, + style = BoothTitle3, + ) + } + } + Spacer(modifier = Modifier.height(20.dp)) + Row(modifier = Modifier.padding(horizontal = 16.dp)) { + UnifestButton( + onClick = onConfirmClick, + contentPadding = PaddingValues(vertical = 16.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(id = R.string.waiting_dialog_waiting_confirm_button), + style = Title5, + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@Composable +fun WaitingCancelDialog( + onCancelClick: () -> Unit, + onConfirmClick: () -> Unit, +) { + UnifestTheme { + UnifestDialog( + onDismissRequest = {}, + titleResId = R.string.waiting_cancel_dialog_title, + iconResId = designR.drawable.ic_caution, + iconDescription = "Caution Icon", + descriptionResId = R.string.waiting_cancel_dialog_description, + confirmTextResId = designR.string.confirm, + cancelTextResId = designR.string.cancel, + onCancelClick = onCancelClick, + onConfirmClick = onConfirmClick, + ) + } +} + +@Composable +fun NoShowWaitingCancelDialog( + onCancelClick: () -> Unit, + onConfirmClick: () -> Unit, +) { + UnifestTheme { + UnifestDialog( + onDismissRequest = {}, + titleResId = R.string.waiting_no_show_dialog_title, + iconResId = designR.drawable.ic_caution, + iconDescription = "Caution Icon", + descriptionResId = R.string.waiting_no_show_dialog_description, + confirmTextResId = designR.string.confirm, + cancelTextResId = designR.string.cancel, + onCancelClick = onCancelClick, + onConfirmClick = onConfirmClick, + ) + } +} + +@ComponentPreview +@Composable +private fun WaitingPinDialogPreview() { + UnifestTheme { + WaitingPinDialog( + boothName = "컴공 주점", + pinNumber = "", + onDismissRequest = {}, + onDialogPinButtonClick = { }, + onPinNumberUpdated = { }, + isWrongPinInserted = true, + ) + } +} + +@ComponentPreview +@Composable +private fun WaitingDialogPreview() { + UnifestTheme { + WaitingDialog( + boothName = "컴공 주점", + onDismissRequest = {}, + phoneNumber = "", + waitingCount = 3, + partySize = 3, + onDialogWaitingButtonClick = { }, + onWaitingMinusClick = { }, + onWaitingPlusClick = { }, + onWaitingTelUpdated = { }, + isPrivacyClicked = false, + onPolicyCheckBoxClick = { }, + onPrivacyPolicyClick = { }, + onThirdPartyPolicyClick = { }, + ) + } +} + +@ComponentPreview +@Composable +private fun WaitingConfirmDialogPreview() { + UnifestTheme { + WaitingConfirmDialog( + boothName = "컴공 주점", + waitingId = 1, + waitingPartySize = 3, + waitingTeamNumber = 3, + onConfirmClick = { }, + ) + } +} diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml new file mode 100644 index 00000000..8a6a1ca1 --- /dev/null +++ b/core/ui/src/main/res/values/strings.xml @@ -0,0 +1,37 @@ + + + 권한 필요 + 앱 설정으로 이동 + + 관심 부스 없음 + 지도에서 관심있는 부스를 추가해주세요 + + 부스 PIN 입력 + 4자리 PIN을 입력해주세요 + 웨이팅 PIN은 부스 운영자에게 문의해주세요! + 올바르지 않은 PIN입니다. 부스 운영자에 문의바랍니다. + PIN 입력 + 현재 내 앞 웨이팅 + 전화번호를 입력해주세요 + 개인정보 처리방침 + + 제 3자 제공방침 + 에 동의합니다 + 웨이팅 신청 + 인원수 + %d 팀 + 웨이팅 등록 완료! + 입장 순서가 되면 안내해드릴게요. + 웨이팅 번호 + %d번 + 인원수 + %d명 + 내 앞 웨이팅 + %d팀 + 완료 + 웨이팅을 취소합니다 + 정말 취소 하시겠습니까? + 부재 웨이팅을 지웁니다 + 문제가 있는 경우 해당 부스 운영자에 문의바랍니다 + + diff --git a/detekt-config.yml b/detekt-config.yml index c2c73df4..36951a0c 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -3,6 +3,7 @@ complexity: threshold: 100 ignoreAnnotated: [ 'Composable' ] CyclomaticComplexMethod: + threshold: 40 ignoreAnnotated: [ 'Composable' ] LongParameterList: active: false @@ -55,7 +56,7 @@ style: ForbiddenComment: active: false UnusedPrivateMember: - ignoreAnnotated: [ 'Preview' ] + ignoreAnnotated: [ 'Preview', 'ComponentPreview', 'DevicePreview' ] ThrowsCount: active: false ReturnCount: diff --git a/feature/booth/build.gradle.kts b/feature/booth/build.gradle.kts index 0e9d21f5..0c4d5bca 100644 --- a/feature/booth/build.gradle.kts +++ b/feature/booth/build.gradle.kts @@ -2,11 +2,29 @@ plugins { alias(libs.plugins.unifest.android.feature) + alias(libs.plugins.kotlin.serialization) // alias(libs.plugins.compose.investigator) } android { namespace = "com.unifest.android.feature.booth" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigField( + "String", + "UNIFEST_PRIVATE_POLICY_URL", + properties["UNIFEST_PRIVATE_POLICY_URL"] as String, + ) + buildConfigField( + "String", + "UNIFEST_THIRD_PARTY_POLICY_URL", + properties["UNIFEST_THIRD_PARTY_POLICY_URL"] as String, + ) + } } dependencies { diff --git a/feature/booth/src/main/AndroidManifest.xml b/feature/booth/src/main/AndroidManifest.xml deleted file mode 100644 index 8bdb7e14..00000000 --- a/feature/booth/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/BoothDetailScreen.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/BoothDetailScreen.kt index 15c8eea5..4ed22993 100644 --- a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/BoothDetailScreen.kt +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/BoothDetailScreen.kt @@ -1,28 +1,22 @@ package com.unifest.android.feature.booth +import android.widget.Toast import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -31,49 +25,38 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.LastBaseline -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.unifest.android.core.common.ObserveAsEvents -import com.unifest.android.core.common.extension.clickableSingle -import com.unifest.android.core.common.utils.formatAsCurrency -import com.unifest.android.core.designsystem.R +import com.unifest.android.core.common.extension.findActivity +import com.unifest.android.core.common.extension.navigateToAppSetting +import com.unifest.android.core.designsystem.R as designR import com.unifest.android.core.designsystem.component.LoadingWheel import com.unifest.android.core.designsystem.component.NetworkErrorDialog import com.unifest.android.core.designsystem.component.NetworkImage import com.unifest.android.core.designsystem.component.ServerErrorDialog import com.unifest.android.core.designsystem.component.TopAppBarNavigationType -import com.unifest.android.core.designsystem.component.UnifestButton -import com.unifest.android.core.designsystem.component.UnifestHorizontalDivider -import com.unifest.android.core.designsystem.component.UnifestOutlinedButton import com.unifest.android.core.designsystem.component.UnifestSnackBar import com.unifest.android.core.designsystem.component.UnifestTopAppBar -import com.unifest.android.core.designsystem.theme.BoothCaution -import com.unifest.android.core.designsystem.theme.BoothLocation -import com.unifest.android.core.designsystem.theme.BoothTitle1 -import com.unifest.android.core.designsystem.theme.Content2 import com.unifest.android.core.designsystem.theme.Content3 -import com.unifest.android.core.designsystem.theme.MainColor -import com.unifest.android.core.designsystem.theme.MenuPrice -import com.unifest.android.core.designsystem.theme.MenuTitle +import com.unifest.android.core.designsystem.theme.DarkGrey100 import com.unifest.android.core.designsystem.theme.Title2 -import com.unifest.android.core.designsystem.theme.Title4 -import com.unifest.android.core.designsystem.theme.Title5 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.android.core.model.BoothDetailModel -import com.unifest.android.core.model.MenuModel import com.unifest.android.core.ui.DevicePreview +import com.unifest.android.core.ui.component.WaitingConfirmDialog +import com.unifest.android.core.ui.component.WaitingDialog +import com.unifest.android.core.ui.component.WaitingPinDialog +import com.unifest.android.feature.booth.component.BoothBottomBar +import com.unifest.android.feature.booth.component.BoothDescription +import com.unifest.android.feature.booth.component.MenuItem +import com.unifest.android.feature.booth.preview.BoothDetailPreviewParameterProvider import com.unifest.android.feature.booth.viewmodel.BoothUiAction import com.unifest.android.feature.booth.viewmodel.BoothUiEvent import com.unifest.android.feature.booth.viewmodel.BoothUiState @@ -97,6 +80,9 @@ internal fun BoothDetailRoute( val context = LocalContext.current val scope = rememberCoroutineScope() val snackBarState = remember { SnackbarHostState() } + val isDarkTheme = isSystemInDarkTheme() + val uriHandler = LocalUriHandler.current + val activity = context.findActivity() DisposableEffect(systemUiController) { systemUiController.setStatusBarColor( @@ -105,8 +91,8 @@ internal fun BoothDetailRoute( ) onDispose { systemUiController.setStatusBarColor( - color = Color.White, - darkIcons = true, + color = if (isDarkTheme) DarkGrey100 else Color.White, + darkIcons = !isDarkTheme, ) } } @@ -115,6 +101,8 @@ internal fun BoothDetailRoute( when (event) { is BoothUiEvent.NavigateBack -> onBackClick() is BoothUiEvent.NavigateToBoothLocation -> navigateToBoothLocation() + is BoothUiEvent.NavigateToPrivatePolicy -> uriHandler.openUri(BuildConfig.UNIFEST_PRIVATE_POLICY_URL) + is BoothUiEvent.NavigateToThirdPartyPolicy -> uriHandler.openUri(BuildConfig.UNIFEST_THIRD_PARTY_POLICY_URL) is BoothUiEvent.ShowSnackBar -> { scope.launch { val job = launch { @@ -127,6 +115,9 @@ internal fun BoothDetailRoute( job.cancel() } } + + is BoothUiEvent.ShowToast -> Toast.makeText(context, event.message.asString(context), Toast.LENGTH_SHORT).show() + is BoothUiEvent.NavigateToAppSetting -> activity.navigateToAppSetting() } } @@ -145,7 +136,11 @@ fun BoothDetailScreen( snackBarState: SnackbarHostState, onAction: (BoothUiAction) -> Unit, ) { - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + ) { BoothDetailContent( uiState = uiState, onAction = onAction, @@ -153,16 +148,17 @@ fun BoothDetailScreen( ) UnifestTopAppBar( navigationType = TopAppBarNavigationType.Back, - navigationIconRes = R.drawable.ic_arrow_back_gray, + navigationIconRes = designR.drawable.ic_arrow_back_gray, containerColor = Color.Transparent, onNavigationClick = { onAction(BoothUiAction.OnBackClick) }, modifier = Modifier .align(Alignment.TopCenter) .padding(padding), ) - BottomBar( + BoothBottomBar( isBookmarked = uiState.isLiked, bookmarkCount = uiState.boothDetailInfo.likes, + isWaitingEnable = uiState.boothDetailInfo.waitingEnabled, onAction = onAction, modifier = Modifier.align(Alignment.BottomCenter), ) @@ -198,6 +194,45 @@ fun BoothDetailScreen( onRetryClick = { onAction(BoothUiAction.OnRetryClick(ErrorType.NETWORK)) }, ) } + + if (uiState.isPinCheckDialogVisible) { + WaitingPinDialog( + boothName = uiState.boothDetailInfo.name, + pinNumber = uiState.boothPinNumber, + onPinNumberUpdated = { onAction(BoothUiAction.OnPinNumberUpdated(it)) }, + onDialogPinButtonClick = { onAction(BoothUiAction.OnDialogPinButtonClick) }, + onDismissRequest = { onAction(BoothUiAction.OnPinDialogDismiss) }, + isWrongPinInserted = uiState.isWrongPinInserted, + ) + } + + if (uiState.isWaitingDialogVisible) { + WaitingDialog( + boothName = uiState.boothDetailInfo.name, + waitingCount = uiState.waitingTeamNumber, + phoneNumber = uiState.waitingTel, + partySize = uiState.waitingPartySize, + isPrivacyClicked = uiState.privacyConsentChecked, + onDismissRequest = { onAction(BoothUiAction.OnWaitingDialogDismiss) }, + onWaitingMinusClick = { onAction(BoothUiAction.OnWaitingMinusClick) }, + onWaitingPlusClick = { onAction(BoothUiAction.OnWaitingPlusClick) }, + onDialogWaitingButtonClick = { onAction(BoothUiAction.OnDialogWaitingButtonClick) }, + onWaitingTelUpdated = { onAction(BoothUiAction.OnWaitingTelUpdated(it)) }, + onPolicyCheckBoxClick = { onAction(BoothUiAction.OnPolicyCheckBoxClick) }, + onPrivacyPolicyClick = { onAction(BoothUiAction.OnPrivatePolicyClick) }, + onThirdPartyPolicyClick = { onAction(BoothUiAction.OnThirdPartyPolicyClick) }, + ) + } + + if (uiState.isConfirmDialogVisible) { + WaitingConfirmDialog( + boothName = uiState.boothDetailInfo.name, + waitingId = uiState.waitingId, + waitingPartySize = uiState.waitingPartySize, + waitingTeamNumber = uiState.waitingTeamNumber, + onConfirmClick = { onAction(BoothUiAction.OnConfirmDialogDismiss) }, + ) + } } } @@ -211,7 +246,14 @@ fun BoothDetailContent( modifier = modifier.fillMaxSize(), ) { item { - BoothImage(uiState.boothDetailInfo.thumbnail) + NetworkImage( + imgUrl = uiState.boothDetailInfo.thumbnail, + contentDescription = "Booth Image", + modifier = Modifier + .height(260.dp) + .fillMaxWidth(), + placeholder = painterResource(id = designR.drawable.image_placeholder), + ) } item { Spacer(modifier = Modifier.height(30.dp)) } item { @@ -220,13 +262,28 @@ fun BoothDetailContent( warning = uiState.boothDetailInfo.warning, description = uiState.boothDetailInfo.description, location = uiState.boothDetailInfo.location, + isRunning = uiState.isRunning, + openTime = uiState.boothDetailInfo.openTime, + closeTime = uiState.boothDetailInfo.closeTime, onAction = onAction, ) } item { Spacer(modifier = Modifier.height(32.dp)) } - item { UnifestHorizontalDivider() } + item { + HorizontalDivider( + thickness = 8.dp, + color = MaterialTheme.colorScheme.outline, + ) + } item { Spacer(modifier = Modifier.height(22.dp)) } - item { MenuText() } + item { + Text( + text = stringResource(id = R.string.booth_menu), + modifier = Modifier.padding(start = 20.dp), + color = MaterialTheme.colorScheme.onBackground, + style = Title2, + ) + } item { Spacer(modifier = Modifier.height(16.dp)) } if (uiState.boothDetailInfo.menus.isEmpty()) { item { @@ -237,7 +294,7 @@ fun BoothDetailContent( Text( text = stringResource(id = R.string.booth_empty_menu), modifier = Modifier.padding(top = 76.dp), - color = Color(0xFF7E7E7E), + color = MaterialTheme.colorScheme.onSurfaceVariant, style = Content3, ) } @@ -256,392 +313,18 @@ fun BoothDetailContent( } } -@Composable -fun BottomBar( - bookmarkCount: Int, - isBookmarked: Boolean, - onAction: (BoothUiAction) -> Unit, - modifier: Modifier = Modifier, -) { - val bookMarkColor = if (isBookmarked) MainColor else Color(0xFF4B4B4B) - Surface( - modifier = modifier.height(116.dp), - shadowElevation = 32.dp, - color = Color.White, - ) { - Box( - modifier = Modifier.fillMaxSize(), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 27.dp, top = 15.dp, end = 15.dp, bottom = 15.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier.background(Color.White), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Icon( - imageVector = ImageVector.vectorResource(if (isBookmarked) R.drawable.ic_bookmarked else R.drawable.ic_bookmark), - contentDescription = if (isBookmarked) "북마크됨" else "북마크하기", - tint = bookMarkColor, - modifier = Modifier.clickableSingle { - onAction(BoothUiAction.OnToggleBookmark) - }, - ) - Text( - text = "$bookmarkCount", - color = bookMarkColor, - style = BoothCaution.copy(fontWeight = FontWeight.Bold), - ) - } - Spacer(modifier = Modifier.width(18.dp)) - UnifestButton( - onClick = { /* showWaitingDialog = true */ }, - modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(vertical = 15.dp), - enabled = false, - containerColor = Color(0xFF777777), - ) { - Text( - text = stringResource(id = R.string.booth_waiting_button_invalid), - style = Title4, - fontSize = 14.sp, - ) - } - } - } - } -} - -@Composable -fun BoothImage( - imgUrl: String, -) { - NetworkImage( - imgUrl = imgUrl, - contentDescription = "Booth Image", - modifier = Modifier - .height(260.dp) - .fillMaxWidth(), - placeholder = painterResource(id = R.drawable.ic_image_placeholder), - ) -} - -@Composable -fun BoothDescription( - name: String, - warning: String, - description: String, - location: String, - onAction: (BoothUiAction) -> Unit, -) { - val configuration = LocalConfiguration.current - val maxWidth = remember(configuration) { - val screenWidth = configuration.screenWidthDp.dp - 40.dp - screenWidth * (2 / 3f) - } - - Column(modifier = Modifier.padding(horizontal = 20.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = name, - modifier = Modifier - .widthIn(max = maxWidth) - .alignBy(LastBaseline), - style = BoothTitle1, - ) - Spacer(modifier = Modifier.width(5.dp)) - Text( - text = warning, - modifier = Modifier.alignBy(LastBaseline), - style = BoothCaution, - color = MainColor, - ) - } - Spacer(modifier = Modifier.height(15.dp)) - Text( - text = description, - modifier = Modifier.padding(top = 8.dp), - style = Content2.copy(lineHeight = 18.sp), - ) - Spacer(modifier = Modifier.height(20.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 8.dp), - ) { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_location_green), - contentDescription = "location icon", - tint = Color.Unspecified, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = location, - color = Color(0xFF393939), - style = BoothLocation, - ) - } - Spacer(modifier = Modifier.height(16.dp)) - UnifestOutlinedButton( - onClick = { onAction(BoothUiAction.OnCheckLocationClick) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = stringResource(id = R.string.booth_check_locaiton), - style = Title5, - ) - } - } -} - -@Composable -fun MenuText() { - Text( - text = stringResource(id = R.string.booth_menu), - modifier = Modifier.padding(start = 20.dp), - style = Title2, - ) -} - -@Composable -fun MenuItem( - menu: MenuModel, - onAction: (BoothUiAction) -> Unit, -) { - Row( - modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp), - ) { - NetworkImage( - imgUrl = menu.imgUrl, - contentDescription = menu.name, - placeholder = painterResource(id = R.drawable.ic_item_placeholder), - modifier = Modifier - .size(86.dp) - .clip(RoundedCornerShape(16.dp)) - .clickable( - onClick = { - if (menu.imgUrl.isNotEmpty()) { - onAction(BoothUiAction.OnMenuImageClick(menu)) - } - }, - ), - ) - Spacer(modifier = Modifier.width(13.dp)) - Column( - modifier = Modifier.align(Alignment.CenterVertically), - ) { - Text( - text = menu.name, - style = MenuTitle, - color = Color(0xFF545454), - ) - Spacer(modifier = Modifier.height(3.dp)) - Text( - text = menu.price.formatAsCurrency(), - style = MenuPrice, - ) - } - } -} - @DevicePreview @Composable -fun BoothScreenPreview() { +private fun BoothScreenPreview( + @PreviewParameter(BoothDetailPreviewParameterProvider::class) + boothUiState: BoothUiState, +) { UnifestTheme { BoothDetailScreen( padding = PaddingValues(), - uiState = BoothUiState( - boothDetailInfo = BoothDetailModel( - id = 0L, - name = "컴공 주점", - category = "컴퓨터공학부 전용 부스", - description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", - warning = "", - location = "청심대 앞", - latitude = 37.54224856023523f, - longitude = 127.07605430700158f, - menus = listOf( - MenuModel(1L, "모둠 사시미", 45000, ""), - MenuModel(2L, "모둠 사시미", 45000, ""), - MenuModel(3L, "모둠 사시미", 45000, ""), - MenuModel(4L, "모둠 사시미", 45000, ""), - ), - ), - ), + uiState = boothUiState, snackBarState = SnackbarHostState(), onAction = {}, ) } } - -// @Composable -// fun WaitingDialog( -// boothName: String, -// onDismissRequest: () -> Unit, -// onWaitingConfirm: (Int, String) -> Unit, -// ) { -// var waitingCount by remember { mutableIntStateOf(0) } -// var phoneNumber by remember { mutableStateOf("") } -// //https://stackoverflow.com/questions/65243956/jetpack-compose-fullscreen-dialog -// Dialog( -// properties = DialogProperties(usePlatformDefaultWidth = false), -// onDismissRequest = onDismissRequest, -// ) { -// Box( -// Modifier -// .background(Color.Black.copy(alpha = 0.6f)) -// .fillMaxSize(), -// ) { -// Card( -// modifier = Modifier -// .align(Alignment.Center) -// .padding(horizontal = 32.dp) -// .background(Color.White), -// ) { -// Column( -// modifier = Modifier -// .padding(16.dp) -// .fillMaxWidth(), -// horizontalAlignment = Alignment.CenterHorizontally, -// ) { -// Row { -// Icon( -// imageVector = ImageVector.vectorResource(id = R.drawable.ic_location_green), -// contentDescription = "location icon", -// tint = Color.Unspecified, -// ) -// Spacer(modifier = Modifier.width(2.dp)) -// Text(text = boothName, style = Title3) -// -// -// } -// -// Spacer(modifier = Modifier.height(5.dp)) -// Text(text = "현재 내 앞 웨이팅", style = BoothLocation) -// Spacer(modifier = Modifier.height(4.dp)) -// -// Text(text = "$waitingCount 팀", style = WaitingTeam) -// Spacer(modifier = Modifier.height(9.dp)) -// -// Row( -// verticalAlignment = Alignment.CenterVertically, -// horizontalArrangement = Arrangement.SpaceBetween, -// ) { -// Text("인원 수", style = Title5, modifier = Modifier.padding(horizontal = 16.dp)) -// Row { -// IconButton( -// onClick = { if (waitingCount > 0) waitingCount-- }, -// ) -// { Icon(Icons.Default.Remove, contentDescription = "Decrease",) } -// Spacer(modifier = Modifier.width(20.dp)) -// Text( -// "$waitingCount", -// Modifier.padding(horizontal = 16.dp), -// ) -// Spacer(modifier = Modifier.width(20.dp)) -// IconButton( -// onClick = { waitingCount++ }, -// ) -// { Icon(Icons.Default.Add, contentDescription = "Increase",) } -// } -// } -// OutlinedTextField( -// value = phoneNumber, -// onValueChange = { phoneNumber = it }, -// label = { Text("전화번호 입력", style = BoothLocation) }, -// singleLine = true, -// ) -// Spacer(modifier = Modifier.height(16.dp)) -// UnifestButton( -// onClick = { onWaitingConfirm(waitingCount, phoneNumber) }, -// modifier = Modifier.fillMaxWidth(), -// ) { -// Text("웨이팅 신청") -// } -// } -// } -// } -// } -// } -// -// -// @Composable -// fun WaitingConfirmDialog( -// boothName: String, -// onDismissRequest: () -> Unit, -// onWaitingConfirm: (Int, String) -> Unit, -// ) { -// //https://stackoverflow.com/questions/65243956/jetpack-compose-fullscreen-dialog -// Dialog( -// properties = DialogProperties(usePlatformDefaultWidth = false), -// onDismissRequest = onDismissRequest -// ) { -// Box( -// Modifier -// .background(Color.Black.copy(alpha = 0.6f)) -// .fillMaxSize(), -// ) { -// Card( -// modifier = Modifier -// .align(Alignment.Center) -// .padding(horizontal = 32.dp) -// .background(Color.White), -// ) { -// Column( -// modifier = Modifier -// .padding(16.dp) -// .fillMaxWidth(), -// horizontalAlignment = Alignment.CenterHorizontally, -// ) { -// Row { -// Icon( -// imageVector = ImageVector.vectorResource(id = R.drawable.ic_location_green), -// contentDescription = "location icon", -// tint = Color.Unspecified, -// ) -// Spacer(modifier = Modifier.width(2.dp)) -// Text(text = boothName, style = Title3) -// -// -// } -// -// Spacer(modifier = Modifier.height(5.dp)) -// Text(text = "웨이팅 등록 완료!", style = Title1) -// Spacer(modifier = Modifier.height(4.dp)) -// Text(text = "입장 순서가 되면 안내 해드릴게요.", style = Content2) -// Spacer(modifier = Modifier.height(16.dp)) -// Row { -// Column { -// Text("웨이팅 번호", style = Title5) -// Text("112번", style = WaitingTeam) -// } -// Spacer(modifier = Modifier.width(40.dp)) -// Column { -// Text("인원수", style = Title5) -// Text("3명", style = WaitingTeam) -// } -// Spacer(modifier = Modifier.width(40.dp)) -// Column { -// Text("내 앞 웨이팅", style = Title5) -// Text("35팀", style = WaitingTeam) -// } -// } -// Spacer(modifier = Modifier.height(16.dp)) -// Row { -// UnifestOutlinedButton(onClick = { /*TODO*/ }, borderColor = Color(0xFFD2D2D2), contentColor = Color.Black) { -// Text(text = "웨이팅 취소") -// } -// Spacer(modifier = Modifier.width(6.dp)) -// UnifestButton(onClick = { /*TODO*/ }) { -// Text(text = "순서 확인하기") -// } -// } -// -// } -// } -// } -// } -// } diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/BoothLocationScreen.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/BoothLocationScreen.kt index 813f78f9..0071f38c 100644 --- a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/BoothLocationScreen.kt +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/BoothLocationScreen.kt @@ -1,36 +1,21 @@ package com.unifest.android.feature.booth -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.naver.maps.geometry.LatLng import com.naver.maps.map.CameraPosition import com.naver.maps.map.compose.ExperimentalNaverMapApi +import com.naver.maps.map.compose.MapProperties import com.naver.maps.map.compose.MapUiSettings import com.naver.maps.map.compose.Marker import com.naver.maps.map.compose.MarkerState @@ -38,11 +23,10 @@ import com.naver.maps.map.compose.NaverMap import com.naver.maps.map.compose.PolygonOverlay import com.naver.maps.map.compose.rememberCameraPositionState import com.unifest.android.core.designsystem.MarkerCategory -import com.unifest.android.core.designsystem.R -import com.unifest.android.core.designsystem.theme.BoothLocation -import com.unifest.android.core.designsystem.theme.Title1 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.android.core.model.BoothDetailModel +import com.unifest.android.core.ui.DevicePreview +import com.unifest.android.feature.booth.component.BoothLocationAppBar +import com.unifest.android.feature.booth.preview.BoothDetailPreviewParameterProvider import com.unifest.android.feature.booth.viewmodel.BoothUiState import com.unifest.android.feature.booth.viewmodel.BoothViewModel import kotlinx.collections.immutable.persistentListOf @@ -68,11 +52,14 @@ fun BoothLocationScreen( ) { Box(modifier = Modifier.fillMaxSize()) { val cameraPositionState = rememberCameraPositionState { - position = CameraPosition(LatLng(37.5420, 127.07673671067072), 14.8) + position = CameraPosition(LatLng(37.0122749, 127.2635972), 15.8) } NaverMap( cameraPositionState = cameraPositionState, modifier = Modifier.fillMaxSize(), + properties = MapProperties( + isNightModeEnabled = isSystemInDarkTheme(), + ), uiSettings = MapUiSettings( isZoomControlEnabled = false, isScaleBarEnabled = false, @@ -102,86 +89,16 @@ fun BoothLocationScreen( } } -@OptIn(ExperimentalMaterial3Api::class) +@DevicePreview @Composable -fun BoothLocationAppBar( - onBackClick: () -> Unit, - boothName: String, - boothLocation: String, - modifier: Modifier = Modifier, +fun BoothLocationScreenPreview( + @PreviewParameter(BoothDetailPreviewParameterProvider::class) + boothUiState: BoothUiState, ) { - TopAppBar( - modifier = modifier - .fillMaxWidth() - .background( - color = Color.White, - shape = RoundedCornerShape(bottomStart = 32.dp, bottomEnd = 32.dp), - ) - .padding(vertical = 8.dp, horizontal = 12.dp), - navigationIcon = { - IconButton(onClick = onBackClick) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_arrow_back_gray), - contentDescription = "뒤로 가기", - ) - } - }, - title = { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = boothName, - style = Title1, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - ) - Text( - text = boothLocation, - style = BoothLocation, - color = Color(0xFF545454), - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors(Color.White), - actions = { - Spacer(modifier = Modifier.width(48.dp)) - }, - ) -} - -@Preview -@Composable -fun BoothLocationScreenPreview() { UnifestTheme { BoothLocationScreen( - uiState = BoothUiState( - boothDetailInfo = BoothDetailModel( - id = 0L, - name = "컴공 주점", - category = "컴퓨터공학부 전용 부스", - description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", - location = "청심대 앞", - latitude = 37.54224856023523f, - longitude = 127.07605430700158f, - ), - ), - onBackClick = {}, - ) - } -} - -@Preview -@Composable -fun BoothLocationAppBarPreview() { - UnifestTheme { - BoothLocationAppBar( + uiState = boothUiState, onBackClick = {}, - boothName = "컴공 주점", - boothLocation = "청심대 앞", ) } } diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/MenuImageDialog.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/MenuImageDialog.kt index 88332cd3..71cd4c13 100644 --- a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/MenuImageDialog.kt +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/MenuImageDialog.kt @@ -22,9 +22,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min import androidx.compose.ui.window.DialogProperties import com.unifest.android.core.common.utils.formatAsCurrency +import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.component.NetworkImage import com.unifest.android.core.designsystem.theme.Content1 import com.unifest.android.core.designsystem.theme.Title2 +import com.unifest.android.core.designsystem.theme.UnifestTheme import com.unifest.android.core.model.MenuModel @OptIn(ExperimentalMaterial3Api::class) @@ -79,3 +81,20 @@ fun MenuImageDialog( } } } + +@ComponentPreview +@Composable +private fun MenuImageDialogPreview() { + UnifestTheme { + MenuImageDialog( + onDismissRequest = {}, + menu = MenuModel( + id = 0, + name = "모둠 사시미", + price = 45000, + imgUrl = "", + status = "10개 미만 남음", + ), + ) + } +} diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/BoothBottomBar.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/BoothBottomBar.kt new file mode 100644 index 00000000..84b7b05b --- /dev/null +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/BoothBottomBar.kt @@ -0,0 +1,114 @@ +package com.unifest.android.feature.booth.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.unifest.android.core.common.extension.clickableSingle +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.component.UnifestButton +import com.unifest.android.core.designsystem.theme.BoothCaution +import com.unifest.android.core.designsystem.theme.Title4 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.feature.booth.R +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.feature.booth.viewmodel.BoothUiAction + +@Composable +fun BoothBottomBar( + bookmarkCount: Int, + isBookmarked: Boolean, + isWaitingEnable: Boolean, + onAction: (BoothUiAction) -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier.height(116.dp), + shadowElevation = 32.dp, + color = MaterialTheme.colorScheme.surface, + ) { + Box( + modifier = Modifier.fillMaxSize(), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 27.dp, top = 15.dp, end = 15.dp, bottom = 15.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.background(MaterialTheme.colorScheme.surface), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = ImageVector.vectorResource(if (isBookmarked) designR.drawable.ic_bookmarked else designR.drawable.ic_bookmark), + contentDescription = if (isBookmarked) "북마크됨" else "북마크하기", + tint = if (isBookmarked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.clickableSingle { + onAction(BoothUiAction.OnToggleBookmark) + }, + ) + Text( + text = "$bookmarkCount", + color = if (isBookmarked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + style = BoothCaution.copy(fontWeight = FontWeight.Bold), + ) + } + Spacer(modifier = Modifier.width(18.dp)) + UnifestButton( + onClick = { onAction(BoothUiAction.OnWaitingButtonClick) }, + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(vertical = 15.dp), + enabled = isWaitingEnable, + containerColor = if (isWaitingEnable) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, + ) { + Text( + text = if (isWaitingEnable) stringResource( + id = R.string.booth_waiting_button, + ) else stringResource( + id = R.string.booth_waiting_button_invalid, + ), + style = Title4, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onPrimary, + ) + } + } + } + } +} + +@ComponentPreview +@Composable +fun BoothBottomBarPreview() { + UnifestTheme { + BoothBottomBar( + bookmarkCount = 12, + isBookmarked = true, + isWaitingEnable = true, + onAction = {}, + ) + } +} diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/BoothDescription.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/BoothDescription.kt new file mode 100644 index 00000000..f3092177 --- /dev/null +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/BoothDescription.kt @@ -0,0 +1,194 @@ +package com.unifest.android.feature.booth.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.LastBaseline +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.unifest.android.core.common.utils.parseAndFormatTime +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.core.designsystem.component.UnifestOutlinedButton +import com.unifest.android.core.designsystem.theme.BoothCaution +import com.unifest.android.core.designsystem.theme.BoothLocation +import com.unifest.android.core.designsystem.theme.BoothTitle1 +import com.unifest.android.core.designsystem.theme.Content2 +import com.unifest.android.core.designsystem.theme.Title5 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.feature.booth.R +import com.unifest.android.feature.booth.viewmodel.BoothUiAction +import java.time.LocalTime + +@Composable +fun BoothDescription( + name: String, + warning: String, + description: String, + location: String, + isRunning: Boolean, + openTime: String, + closeTime: String, + onAction: (BoothUiAction) -> Unit, +) { + val configuration = LocalConfiguration.current + val maxWidth = remember(configuration) { + val screenWidth = configuration.screenWidthDp.dp - 40.dp + screenWidth * (2 / 3f) + } + + val (openTimeFormatted, openLocalTime) = parseAndFormatTime(openTime) + val (closeTimeFormatted, closeLocalTime) = parseAndFormatTime(closeTime) + + // 현재 시간 가져오기 + val currentTime = LocalTime.now() + + // 부스 운영 여부 확인 + val isBoothRunning = openLocalTime != null && closeLocalTime != null && + currentTime.isAfter(openLocalTime) && currentTime.isBefore(closeLocalTime) + + // 부스 운영 여부 확인 안됨 + val isBoothRunningDetailProvided = openLocalTime != null && closeLocalTime != null + + Column( + modifier = Modifier + .padding(horizontal = 20.dp) + .animateContentSize(), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = name, + modifier = Modifier + .widthIn(max = maxWidth) + .alignBy(LastBaseline), + color = MaterialTheme.colorScheme.onBackground, + style = BoothTitle1, + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = warning, + modifier = Modifier.alignBy(LastBaseline), + style = BoothCaution, + color = MaterialTheme.colorScheme.primary, + ) + } + Spacer(modifier = Modifier.height(15.dp)) + Text( + text = description, + modifier = Modifier.padding(top = 8.dp), + color = MaterialTheme.colorScheme.onSecondary, + style = Content2.copy(lineHeight = 18.sp), + ) + Spacer(modifier = Modifier.height(22.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(top = 8.dp) + .clickable { onAction(BoothUiAction.OnRunningClick) }, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_clock), + contentDescription = "location icon", + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = if (!isBoothRunningDetailProvided) { + stringResource(id = R.string.booth_is_unknown_running) + } else if (isBoothRunning) { + stringResource(id = R.string.booth_is_running) + } else { + stringResource(id = R.string.booth_is_closed) + }, + color = MaterialTheme.colorScheme.onBackground, + style = BoothLocation, + ) + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_arrow_below), + contentDescription = "arrow below", + tint = Color.Unspecified, + ) + } + AnimatedVisibility(visible = isRunning) { + Column { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Open Time: $openTimeFormatted", + color = MaterialTheme.colorScheme.onBackground, + style = BoothLocation, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Close Time: $closeTimeFormatted", + color = MaterialTheme.colorScheme.onBackground, + style = BoothLocation, + ) + } + } + Spacer(modifier = Modifier.height(11.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 8.dp), + ) { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_location_green), + contentDescription = "location icon", + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = location, + color = MaterialTheme.colorScheme.onBackground, + style = BoothLocation, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + UnifestOutlinedButton( + onClick = { onAction(BoothUiAction.OnCheckLocationClick) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(id = R.string.booth_check_locaiton), + style = Title5, + ) + } + } +} + +@ComponentPreview +@Composable +fun BoothDescriptionPreview() { + UnifestTheme { + BoothDescription( + name = "공대주점", + warning = "누구나 환영", + description = "컴퓨터 공학과와 물리학과가 함께하는 협동부스입니다. 방문자 이벤트로 무료 안주 하나씩 제공중이에요!!", + location = "공학관", + isRunning = true, + openTime = "10:00", + closeTime = "22:00", + onAction = {}, + ) + } +} diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/BoothLocationAppBar.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/BoothLocationAppBar.kt new file mode 100644 index 00000000..3d4d3dfc --- /dev/null +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/BoothLocationAppBar.kt @@ -0,0 +1,94 @@ +package com.unifest.android.feature.booth.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.core.designsystem.theme.BoothLocation +import com.unifest.android.core.designsystem.theme.Title1 +import com.unifest.android.core.designsystem.theme.UnifestTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BoothLocationAppBar( + onBackClick: () -> Unit, + boothName: String, + boothLocation: String, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(bottomStart = 32.dp, bottomEnd = 32.dp), + ) + .padding(vertical = 8.dp, horizontal = 12.dp), + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_arrow_back_gray), + contentDescription = "뒤로 가기", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + title = { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = boothName, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center, + style = Title1, + ) + Text( + text = boothLocation, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + style = BoothLocation, + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surface), + actions = { + Spacer(modifier = Modifier.width(48.dp)) + }, + ) +} + +@ComponentPreview +@Composable +fun BoothLocationAppBarPreview() { + UnifestTheme { + BoothLocationAppBar( + onBackClick = {}, + boothName = "컴공 주점", + boothLocation = "청심대 앞", + ) + } +} diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/MenuItem.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/MenuItem.kt new file mode 100644 index 00000000..7dcf6ae8 --- /dev/null +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/MenuItem.kt @@ -0,0 +1,140 @@ +package com.unifest.android.feature.booth.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.unifest.android.core.common.utils.formatAsCurrency +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.core.designsystem.component.NetworkImage +import com.unifest.android.core.designsystem.theme.Content9 +import com.unifest.android.core.designsystem.theme.MenuPrice +import com.unifest.android.core.designsystem.theme.MenuTitle +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.model.MenuModel +import com.unifest.android.feature.booth.R +import com.unifest.android.feature.booth.viewmodel.BoothUiAction + +@Composable +fun MenuItem( + menu: MenuModel, + onAction: (BoothUiAction) -> Unit, +) { + Row( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp), + ) { + Box( + modifier = Modifier + .size(86.dp) + .clip(RoundedCornerShape(16.dp)) + .clickable( + onClick = { + if (menu.imgUrl.isNotEmpty()) { + onAction(BoothUiAction.OnMenuImageClick(menu)) + } + }, + ), + contentAlignment = Alignment.Center, + ) { + NetworkImage( + imgUrl = menu.imgUrl, + contentDescription = menu.name, + placeholder = painterResource(id = designR.drawable.item_placeholder), + modifier = Modifier.matchParentSize(), + ) + + if (menu.status == "SOLD_OUT") { + Box( + modifier = Modifier + .matchParentSize() + .background(Color.Black.copy(alpha = 0.6f)), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(R.string.sold_out), + color = Color.White, + style = Content9, + ) + } + } + } + Spacer(modifier = Modifier.width(13.dp)) + Column( + modifier = Modifier.align(Alignment.CenterVertically), + ) { + Text( + text = menu.name, + color = if (menu.status == "SOLD_OUT") { + MaterialTheme.colorScheme.secondaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + style = MenuTitle, + ) + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = menu.price.formatAsCurrency(), + color = if (menu.status == "SOLD_OUT") { + MaterialTheme.colorScheme.surfaceVariant + } else { + MaterialTheme.colorScheme.onBackground + }, + style = MenuPrice, + ) + Spacer(modifier = Modifier.height(14.dp)) + Tag(menuStatus = menu.status) + } + } +} + +@ComponentPreview +@Composable +fun MenuItemPreview() { + UnifestTheme { + MenuItem( + menu = MenuModel( + id = 1L, + name = "닭강정", + price = 6000, + imgUrl = "", + status = "ENOUGH", + ), + onAction = {}, + ) + } +} + +@ComponentPreview +@Composable +fun MenuItemSoldOutPreview() { + UnifestTheme { + MenuItem( + menu = MenuModel( + id = 1L, + name = "닭강정", + price = 6000, + imgUrl = "", + status = "SOLD_OUT", + ), + onAction = {}, + ) + } +} diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/MenuStatusTag.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/MenuStatusTag.kt new file mode 100644 index 00000000..f4c526ac --- /dev/null +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/component/MenuStatusTag.kt @@ -0,0 +1,96 @@ +package com.unifest.android.feature.booth.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import com.unifest.android.core.designsystem.ComponentPreview +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.theme.BottomMenuBar +import com.unifest.android.core.designsystem.theme.Content7 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.feature.booth.R + +@Composable +fun Tag( + modifier: Modifier = Modifier, + menuStatus: String = "", +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(7.dp)) + .background(MaterialTheme.colorScheme.outline) + .padding(horizontal = 12.dp, vertical = 4.dp), + ) { + when (menuStatus) { + "SOLD_OUT" -> { + Text( + text = stringResource(R.string.sold_out), + style = Content7, + color = MaterialTheme.colorScheme.error, + ) + } + + "UNDER_10" -> { + val parts = "10개 미만 남음".split("남음") + Text( + buildAnnotatedString { + append(parts[0]) // "10개 미만" + withStyle(style = SpanStyle(fontSize = BottomMenuBar.fontSize, fontWeight = BottomMenuBar.fontWeight)) { + append("남음") // "남음" 부분 스타일 적용 + } + }, + style = Content7, + color = MaterialTheme.colorScheme.surfaceVariant, + ) + } + + "UNDER_50" -> { + val parts = "50개 미만 남음".split("남음") + Text( + buildAnnotatedString { + append(parts[0]) // "50개 미만" + withStyle(style = SpanStyle(fontSize = BottomMenuBar.fontSize, fontWeight = BottomMenuBar.fontWeight)) { + append("남음") // "남음" 부분 스타일 적용 + } + }, + style = Content7, + color = MaterialTheme.colorScheme.surfaceVariant, + ) + } + + "ENOUGH" -> { + Text( + text = stringResource(R.string.enough_status), + style = Content7, + color = MaterialTheme.colorScheme.surfaceVariant, + ) + } + + else -> { + Text( + text = stringResource(id = R.string.no_menu_status), + style = Content7, + color = MaterialTheme.colorScheme.surfaceVariant, + ) + } + } + } +} + +@ComponentPreview +@Composable +fun TagPreview() { + UnifestTheme { + Tag(menuStatus = "10개 미만 남음") + } +} diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/navigation/BoothNavigation.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/navigation/BoothNavigation.kt index 44225cc3..e8b2389a 100644 --- a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/navigation/BoothNavigation.kt +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/navigation/BoothNavigation.kt @@ -4,28 +4,22 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController -import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.compose.navigation -import androidx.navigation.navArgument import com.unifest.android.core.common.extension.sharedViewModel -import com.unifest.android.feature.booth.BoothLocationRoute +import com.unifest.android.core.navigation.Route import com.unifest.android.feature.booth.BoothDetailRoute +import com.unifest.android.feature.booth.BoothLocationRoute import com.unifest.android.feature.booth.viewmodel.BoothViewModel -const val BOOTH_ID = "booth_id" -const val BOOTH_ROUTE = "booth_route/{$BOOTH_ID}" -const val BOOTH_DETAIL_ROUTE = "booth_detail_route" -const val BOOTH_LOCATION_ROUTE = "booth_location_route" - fun NavController.navigateToBoothDetail( boothId: Long, ) { - navigate("booth_route/$boothId") + navigate(Route.Booth.BoothDetail(boothId)) } fun NavController.navigateToBoothLocation() { - navigate(BOOTH_LOCATION_ROUTE) + navigate(Route.Booth.BoothLocation) } fun NavGraphBuilder.boothNavGraph( @@ -34,17 +28,11 @@ fun NavGraphBuilder.boothNavGraph( popBackStack: () -> Unit, navigateToBoothLocation: () -> Unit, ) { - navigation( - startDestination = BOOTH_DETAIL_ROUTE, - route = BOOTH_ROUTE, - arguments = listOf( - navArgument(BOOTH_ID) { - type = NavType.LongType - }, - ), + navigation( + startDestination = Route.Booth.BoothDetail::class, ) { - composable(route = BOOTH_DETAIL_ROUTE) { entry -> - val viewModel = entry.sharedViewModel(navController) + composable { navBackStackEntry -> + val viewModel = navBackStackEntry.sharedViewModel(navController) BoothDetailRoute( padding = padding, onBackClick = popBackStack, @@ -52,8 +40,8 @@ fun NavGraphBuilder.boothNavGraph( viewModel = viewModel, ) } - composable(route = BOOTH_LOCATION_ROUTE) { entry -> - val viewModel = entry.sharedViewModel(navController) + composable { navBackStackEntry -> + val viewModel = navBackStackEntry.sharedViewModel(navController) BoothLocationRoute( onBackClick = popBackStack, viewModel = viewModel, diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/preview/BoothDetailPreviewParameterProvider.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/preview/BoothDetailPreviewParameterProvider.kt new file mode 100644 index 00000000..46e4f7fe --- /dev/null +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/preview/BoothDetailPreviewParameterProvider.kt @@ -0,0 +1,29 @@ +package com.unifest.android.feature.booth.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.unifest.android.core.model.BoothDetailModel +import com.unifest.android.core.model.MenuModel +import com.unifest.android.feature.booth.viewmodel.BoothUiState + +internal class BoothDetailPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + BoothUiState( + boothDetailInfo = BoothDetailModel( + id = 0L, + name = "컴공 주점", + category = "컴퓨터공학부 전용 부스", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", + warning = "", + location = "청심대 앞", + latitude = 37.54224856023523f, + longitude = 127.07605430700158f, + menus = listOf( + MenuModel(1L, "모둠 사시미", 45000, "", "10개 미만 남음"), + MenuModel(2L, "모둠 사시미", 45000, "", "품절"), + MenuModel(3L, "모둠 사시미", 45000, "", "50개 미만 남음"), + MenuModel(4L, "모둠 사시미", 45000, "", "품절임막 5개 미만 남음"), + ), + ), + ), + ) +} diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiAction.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiAction.kt index 73e63813..5214e192 100644 --- a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiAction.kt +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiAction.kt @@ -9,6 +9,20 @@ sealed interface BoothUiAction { data class OnRetryClick(val error: ErrorType) : BoothUiAction data class OnMenuImageClick(val menu: MenuModel) : BoothUiAction data object OnMenuImageDialogDismiss : BoothUiAction + data object OnWaitingButtonClick : BoothUiAction + data object OnDialogPinButtonClick : BoothUiAction + data object OnDialogWaitingButtonClick : BoothUiAction + data class OnPinNumberUpdated(val pinNumber: String) : BoothUiAction + data class OnWaitingTelUpdated(val tel: String) : BoothUiAction + data object OnWaitingDialogDismiss : BoothUiAction + data object OnConfirmDialogDismiss : BoothUiAction + data object OnPinDialogDismiss : BoothUiAction + data object OnWaitingMinusClick : BoothUiAction + data object OnWaitingPlusClick : BoothUiAction + data object OnPolicyCheckBoxClick : BoothUiAction + data object OnPrivatePolicyClick : BoothUiAction + data object OnThirdPartyPolicyClick : BoothUiAction + data object OnRunningClick : BoothUiAction } enum class ErrorType { diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiEvent.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiEvent.kt index c68ee086..92eac103 100644 --- a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiEvent.kt +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiEvent.kt @@ -6,4 +6,8 @@ sealed interface BoothUiEvent { data object NavigateBack : BoothUiEvent data object NavigateToBoothLocation : BoothUiEvent data class ShowSnackBar(val message: UiText) : BoothUiEvent + data object NavigateToPrivatePolicy : BoothUiEvent + data object NavigateToThirdPartyPolicy : BoothUiEvent + data class ShowToast(val message: UiText) : BoothUiEvent + data object NavigateToAppSetting : BoothUiEvent } diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiState.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiState.kt index f8b8a933..b3a75013 100644 --- a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiState.kt +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothUiState.kt @@ -4,6 +4,7 @@ import com.naver.maps.geometry.LatLng import com.unifest.android.core.model.BoothDetailModel import com.unifest.android.core.model.LikedBoothModel import com.unifest.android.core.model.MenuModel +import com.unifest.android.core.model.MyWaitingModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -14,8 +15,21 @@ data class BoothUiState( val isLiked: Boolean = false, val isServerErrorDialogVisible: Boolean = false, val isNetworkErrorDialogVisible: Boolean = false, + val isPinCheckDialogVisible: Boolean = false, + val isWaitingDialogVisible: Boolean = false, + val isConfirmDialogVisible: Boolean = false, val isMenuImageDialogVisible: Boolean = false, + val isWrongPinInserted: Boolean = false, val selectedMenu: MenuModel? = null, + val boothPinNumber: String = "", + val boothPinNumberError: Boolean = false, + val waitingPartySize: Long = 1, + val waitingTel: String = "", + val waitingTeamNumber: Long = 0, + val waitingId: Long = 0, + val privacyConsentChecked: Boolean = false, + val isRunning: Boolean = false, + val myWaitingList: ImmutableList = persistentListOf(), val outerCords: ImmutableList = persistentListOf( LatLng(50.0, 150.0), LatLng(50.0, 100.0), @@ -23,42 +37,39 @@ data class BoothUiState( LatLng(30.0, 150.0), ), val innerHole: ImmutableList = persistentListOf( - LatLng(37.54470, 127.07615), - LatLng(37.54461, 127.07561), - LatLng(37.54478, 127.07553), - LatLng(37.54462, 127.07507), - LatLng(37.54461, 127.07455), - LatLng(37.54470, 127.07396), - LatLng(37.54462, 127.07348), - LatLng(37.54473, 127.07293), - LatLng(37.54195, 127.07162), - LatLng(37.54183, 127.07218), - LatLng(37.54100, 127.07311), - LatLng(37.53950, 127.07238), - LatLng(37.53873, 127.07504), - LatLng(37.53933, 127.07516), - LatLng(37.53919, 127.07674), - LatLng(37.53908, 127.07719), - LatLng(37.53910, 127.07839), - LatLng(37.53900, 127.07850), - LatLng(37.53902, 127.07903), - LatLng(37.53886, 127.07906), - LatLng(37.53891, 127.07995), - LatLng(37.53925, 127.07993), - LatLng(37.53934, 127.07973), - LatLng(37.53982, 127.07962), - LatLng(37.54014, 127.07999), - LatLng(37.54067, 127.08086), - LatLng(37.54119, 127.08131), - LatLng(37.54208, 127.08131), - LatLng(37.54234, 127.08115), - LatLng(37.54257, 127.07857), - LatLng(37.54382, 127.07876), - LatLng(37.54394, 127.07966), - LatLng(37.54429, 127.08006), - LatLng(37.54496, 127.07994), - LatLng(37.54493, 127.07897), - LatLng(37.54485, 127.07754), - LatLng(37.54494, 127.07704), + LatLng(37.0125281, 127.2598307), + LatLng(37.0101251, 127.2631513), + LatLng(37.0095553, 127.2639023), + LatLng(37.0094097, 127.2642242), + LatLng(37.0096453, 127.264546), + LatLng(37.0098295, 127.2649537), + LatLng(37.0100737, 127.2654687), + LatLng(37.0102578, 127.2656458), + LatLng(37.010502, 127.2659032), + LatLng(37.0112431, 127.2661661), + LatLng(37.0113802, 127.2661862), + LatLng(37.0114947, 127.2661782), + LatLng(37.0122818, 127.2659301), + LatLng(37.0125174, 127.2658979), + LatLng(37.0127305, 127.2659019), + LatLng(37.0130015, 127.2659703), + LatLng(37.0130464, 127.2657544), + LatLng(37.0131846, 127.2654915), + LatLng(37.0132178, 127.2654245), + LatLng(37.0133816, 127.2649189), + LatLng(37.0135626, 127.2649269), + LatLng(37.0136301, 127.2645246), + LatLng(37.013629, 127.2643958), + LatLng(37.0134652, 127.2634343), + LatLng(37.0141269, 127.2632143), + LatLng(37.0140359, 127.262694), + LatLng(37.0139781, 127.2626028), + LatLng(37.013266, 127.2617921), + LatLng(37.0133602, 127.2616184), + LatLng(37.0133709, 127.2615789), + LatLng(37.0128826, 127.2611242), + LatLng(37.0128044, 127.2610478), + LatLng(37.0131423, 127.2604087), + LatLng(37.0125265, 127.2598301), ), ) diff --git a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothViewModel.kt b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothViewModel.kt index b9cd2bd8..a00c6c22 100644 --- a/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothViewModel.kt +++ b/feature/booth/src/main/kotlin/com/unifest/android/feature/booth/viewmodel/BoothViewModel.kt @@ -8,12 +8,15 @@ import com.unifest.android.core.common.UiText import com.unifest.android.core.common.handleException import com.unifest.android.core.data.repository.BoothRepository import com.unifest.android.core.data.repository.LikedBoothRepository -import com.unifest.android.core.designsystem.R +import com.unifest.android.core.data.repository.LikedFestivalRepository +import com.unifest.android.core.data.repository.WaitingRepository import com.unifest.android.core.model.MenuModel -import com.unifest.android.feature.booth.navigation.BOOTH_ID +import com.unifest.android.feature.booth.R +import com.unifest.android.core.designsystem.R as designR import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,8 +30,14 @@ import javax.inject.Inject class BoothViewModel @Inject constructor( private val boothRepository: BoothRepository, private val likedBoothRepository: LikedBoothRepository, + private val likedFestivalRepository: LikedFestivalRepository, + private val waitingRepository: WaitingRepository, savedStateHandle: SavedStateHandle, ) : ViewModel(), ErrorHandlerActions { + companion object { + private const val BOOTH_ID = "boothId" + } + private val boothId: Long = requireNotNull(savedStateHandle.get(BOOTH_ID)) { "boothId is required." } private val _uiState = MutableStateFlow(BoothUiState()) @@ -40,6 +49,7 @@ class BoothViewModel @Inject constructor( init { getBoothDetail() getLikedBooths() + getMyWaitingList() } fun onAction(action: BoothUiAction) { @@ -50,6 +60,53 @@ class BoothViewModel @Inject constructor( is BoothUiAction.OnRetryClick -> refresh(action.error) is BoothUiAction.OnMenuImageClick -> showMenuImageDialog(action.menu) is BoothUiAction.OnMenuImageDialogDismiss -> hideMenuImageDialog() + is BoothUiAction.OnPinNumberUpdated -> updatePinNumberText(action.pinNumber) + is BoothUiAction.OnWaitingTelUpdated -> updateWaitingTelText(action.tel) + is BoothUiAction.OnWaitingButtonClick -> checkMyWaitingListNumbers() + is BoothUiAction.OnDialogPinButtonClick -> checkPinValidation() + is BoothUiAction.OnDialogWaitingButtonClick -> requestBoothWaiting() + is BoothUiAction.OnWaitingDialogDismiss -> setWaitingDialogVisible(false) + is BoothUiAction.OnPinDialogDismiss -> setPinCheckDialogVisible(false) + is BoothUiAction.OnConfirmDialogDismiss -> setConfirmDialogVisible(false) + is BoothUiAction.OnWaitingMinusClick -> minusWaitingPartySize() + is BoothUiAction.OnWaitingPlusClick -> plusWaitingPartySize() + is BoothUiAction.OnPolicyCheckBoxClick -> privacyConsentClick() + is BoothUiAction.OnPrivatePolicyClick -> navigateToPrivatePolicy() + is BoothUiAction.OnThirdPartyPolicyClick -> navigateToThirdPartyPolicy() + is BoothUiAction.OnRunningClick -> expandRunningTime() + } + } + + private fun checkMyWaitingListNumbers() { + viewModelScope.launch { + val isAlreadyInWaitingList = _uiState.value.myWaitingList.any { it.boothId == _uiState.value.boothDetailInfo.id } + when { + isAlreadyInWaitingList -> { + _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(R.string.booth_waiting_already_exists))) + } + + _uiState.value.myWaitingList.size >= 3 -> { + _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(R.string.booth_waiting_full))) + } + + else -> { + setPinCheckDialogVisible(true) + } + } + } + } + + private fun getMyWaitingList() { + viewModelScope.launch { + waitingRepository.getMyWaitingList() + .onSuccess { waitingLists -> + _uiState.update { + it.copy(myWaitingList = waitingLists.toImmutableList()) + } + } + .onFailure { exception -> + handleException(exception, this@BoothViewModel) + } } } @@ -126,10 +183,22 @@ class BoothViewModel @Inject constructor( } } + private fun expandRunningTime() { + _uiState.update { + it.copy(isRunning = !it.isRunning) + } + } + private fun toggleBookmark() { val currentBookmarkFlag = _uiState.value.isLiked val newBookmarkFlag = !currentBookmarkFlag viewModelScope.launch { + if (currentBookmarkFlag) { + unregisterLikedFestival() + } else { + registerLikedFestival() + } + boothRepository.likeBooth(boothId) .onSuccess { _uiState.update { @@ -142,22 +211,42 @@ class BoothViewModel @Inject constructor( } if (currentBookmarkFlag) { likedBoothRepository.deleteLikedBooth(_uiState.value.boothDetailInfo) - _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(R.string.liked_booth_removed_message))) + _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(designR.string.liked_booth_removed_message))) } else { likedBoothRepository.insertLikedBooth(_uiState.value.boothDetailInfo) - _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(R.string.liked_booth_saved_message))) + _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(designR.string.liked_booth_saved_message))) } } .onFailure { if (currentBookmarkFlag) { - _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(R.string.liked_booth_removed_failed_message))) + _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(designR.string.liked_booth_removed_failed_message))) } else { - _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(R.string.liked_booth_saved_failed_message))) + _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(designR.string.liked_booth_saved_failed_message))) } } } } + private fun registerLikedFestival() { + viewModelScope.launch { + likedFestivalRepository.registerLikedFestival() + .onSuccess {} + .onFailure { + _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(designR.string.liked_booth_saved_failed_message))) + } + } + } + + private fun unregisterLikedFestival() { + viewModelScope.launch { + likedFestivalRepository.unregisterLikedFestival() + .onSuccess {} + .onFailure { + _uiEvent.send(BoothUiEvent.ShowSnackBar(UiText.StringResource(designR.string.liked_booth_removed_failed_message))) + } + } + } + private fun refresh(error: ErrorType) { getBoothDetail() when (error) { @@ -166,6 +255,118 @@ class BoothViewModel @Inject constructor( } } + private fun requestBoothWaiting() { + val tel = _uiState.value.waitingTel + val partySize = _uiState.value.waitingPartySize + + if (isTelValid(tel) && isPartySizeValid(partySize)) { + viewModelScope.launch { + boothRepository.requestBoothWaiting( + boothId = _uiState.value.boothDetailInfo.id, + tel = tel, + partySize = partySize, + pinNumber = _uiState.value.boothPinNumber, + ).onSuccess { waiting -> + _uiState.update { + it.copy( + waitingId = waiting.waitingId, + waitingTel = "", + boothPinNumber = "", + ) + } + waitingRepository.registerFCMTopic(waiting.waitingId.toString()) + setWaitingDialogVisible(false) + setConfirmDialogVisible(true) + }.onFailure { exception -> + handleException(exception, this@BoothViewModel) + } + } + } else { + viewModelScope.launch { + _uiEvent.send(BoothUiEvent.ShowToast(UiText.StringResource(R.string.booth_empty_waiting))) + } + } + } + + private fun isTelValid(tel: String): Boolean { + return tel.matches(Regex("^010\\d{8}$")) + } + + private fun isPartySizeValid(partySize: Long): Boolean { + return partySize >= 1 + } + + private fun updatePinNumberText(pinNumber: String) { + _uiState.update { + it.copy( + boothPinNumber = pinNumber, + ) + } + } + + private fun updateWaitingTelText(tel: String) { + _uiState.update { + it.copy( + waitingTel = tel, + ) + } + } + + private fun checkPinValidation() { + if (_uiState.value.isWrongPinInserted) { + return + } + + viewModelScope.launch { + boothRepository.checkPinValidation(_uiState.value.boothDetailInfo.id, _uiState.value.boothPinNumber) + .onSuccess { waitingTeamNumber -> + if (waitingTeamNumber > -1) { + _uiState.update { + it.copy(waitingTeamNumber = waitingTeamNumber) + } + setPinCheckDialogVisible(false) + setWaitingDialogVisible(true) + } else { + _uiState.update { + it.copy( + isWrongPinInserted = true, + boothPinNumber = "", + ) + } + delay(2000L) + _uiState.update { + it.copy(isWrongPinInserted = false) + } + } + } + .onFailure { exception -> + handleException(exception, this@BoothViewModel) + } + } + } + + private fun privacyConsentClick() { + _uiState.update { + it.copy(privacyConsentChecked = !it.privacyConsentChecked) + } + } + + private fun minusWaitingPartySize() { + if (_uiState.value.waitingPartySize <= 1) return + + _uiState.update { currentState -> + currentState.copy(waitingPartySize = currentState.waitingPartySize - 1) + } + } + + private fun plusWaitingPartySize() { + if (_uiState.value.waitingPartySize >= 100) return + + _uiState.update { currentState -> + currentState.copy(waitingPartySize = currentState.waitingPartySize + 1) + } + } + private fun showMenuImageDialog(menu: MenuModel) { _uiState.update { it.copy( @@ -184,6 +385,36 @@ class BoothViewModel @Inject constructor( } } + private fun navigateToPrivatePolicy() { + viewModelScope.launch { + _uiEvent.send(BoothUiEvent.NavigateToPrivatePolicy) + } + } + + private fun navigateToThirdPartyPolicy() { + viewModelScope.launch { + _uiEvent.send(BoothUiEvent.NavigateToThirdPartyPolicy) + } + } + + private fun setPinCheckDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isPinCheckDialogVisible = flag) + } + } + + private fun setWaitingDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isWaitingDialogVisible = flag) + } + } + + private fun setConfirmDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isConfirmDialogVisible = flag) + } + } + override fun setServerErrorDialogVisible(flag: Boolean) { _uiState.update { it.copy(isServerErrorDialogVisible = flag) diff --git a/feature/booth/src/main/res/drawable/ic_clock.xml b/feature/booth/src/main/res/drawable/ic_clock.xml new file mode 100644 index 00000000..625074a0 --- /dev/null +++ b/feature/booth/src/main/res/drawable/ic_clock.xml @@ -0,0 +1,14 @@ + + + + diff --git a/feature/booth/src/main/res/values/strings.xml b/feature/booth/src/main/res/values/strings.xml new file mode 100644 index 00000000..10f2b31d --- /dev/null +++ b/feature/booth/src/main/res/values/strings.xml @@ -0,0 +1,19 @@ + + + 위치 확인하기 + 메뉴 + 웨이팅 하기 + 웨이팅 기능을 지원하지 않는 부스입니다 + 등록된 메뉴가 없어요. + 올바른 전화번호를 입력해 주세요! + 운영 시간 + 운영중 + 운영종료 + 웨이팅은 최대 3부스까지 가능해요! + 이미 웨이팅 한 부스에요! + 품절 + 품절 임박 5개 미만 남음 + 등록된 정보 없음 + 여유 재고 + + diff --git a/feature/festival/build.gradle.kts b/feature/festival/build.gradle.kts index 234de391..9941fa01 100644 --- a/feature/festival/build.gradle.kts +++ b/feature/festival/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { projects.core.data, libs.kotlinx.collections.immutable, + libs.compose.system.ui.controller, libs.timber, ) } diff --git a/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/FestivalBottomSheet.kt b/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/FestivalBottomSheet.kt index 99f32698..3c6b2eb8 100644 --- a/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/FestivalBottomSheet.kt +++ b/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/FestivalBottomSheet.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding @@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -24,24 +26,24 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.component.FestivalSearchTextField import com.unifest.android.core.designsystem.component.LikedFestivalDeleteDialog -import com.unifest.android.core.designsystem.component.UnifestHorizontalDivider import com.unifest.android.core.designsystem.theme.Content3 import com.unifest.android.core.designsystem.theme.Title3 import com.unifest.android.core.designsystem.theme.UnifestTheme import com.unifest.android.core.model.FestivalModel import com.unifest.android.core.ui.component.LikedFestivalsGrid +import com.unifest.android.feature.festival.preview.FestivalPreviewParameterProvider import com.unifest.android.feature.festival.viewmodel.ButtonType import com.unifest.android.feature.festival.viewmodel.FestivalUiAction -import com.unifest.android.core.designsystem.R +import com.unifest.android.feature.festival.viewmodel.FestivalUiState import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf +import com.unifest.android.core.designsystem.R as designR @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -65,6 +67,7 @@ fun FestivalSearchBottomSheet( // ) val bottomSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true, + // confirmValueChange = { it != SheetValue.Hidden }, ) ModalBottomSheet( @@ -73,17 +76,18 @@ fun FestivalSearchBottomSheet( }, sheetState = bottomSheetState, shape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp), - containerColor = Color.White, + containerColor = MaterialTheme.colorScheme.surface, dragHandle = { Column( modifier = Modifier .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) .padding(top = 10.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { HorizontalDivider( thickness = 5.dp, - color = Color(0xFFA0A0A0), + color = MaterialTheme.colorScheme.onSecondaryContainer, modifier = Modifier .width(80.dp) .clip(RoundedCornerShape(43.dp)), @@ -97,7 +101,8 @@ fun FestivalSearchBottomSheet( ) { Column( modifier = Modifier - .background(Color.White) + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) .navigationBarsPadding(), ) { Spacer(modifier = Modifier.height(24.dp)) @@ -116,7 +121,10 @@ fun FestivalSearchBottomSheet( ) if (!isSearchMode) { Spacer(modifier = Modifier.height(39.dp)) - UnifestHorizontalDivider() + HorizontalDivider( + thickness = 8.dp, + color = MaterialTheme.colorScheme.scrim, + ) Spacer(modifier = Modifier.height(21.dp)) Row( verticalAlignment = Alignment.CenterVertically, @@ -126,7 +134,8 @@ fun FestivalSearchBottomSheet( .padding(horizontal = 20.dp), ) { Text( - text = stringResource(id = R.string.intro_liked_festivals_title), + text = stringResource(id = designR.string.liked_festivals_title), + color = MaterialTheme.colorScheme.onBackground, style = Title3, ) TextButton( @@ -136,7 +145,7 @@ fun FestivalSearchBottomSheet( ) { Text( text = stringResource(id = R.string.edit), - color = Color.Black, + color = MaterialTheme.colorScheme.onSurfaceVariant, style = Content3, ) } @@ -159,154 +168,35 @@ fun FestivalSearchBottomSheet( ) } } - if (isLikedFestivalDeleteDialogVisible) { - LikedFestivalDeleteDialog( - onCancelClick = { - onFestivalUiAction( - FestivalUiAction.OnDeleteDialogButtonClick(ButtonType.CANCEL), - ) - }, - onConfirmClick = { - onFestivalUiAction( - FestivalUiAction.OnDeleteDialogButtonClick(ButtonType.CONFIRM), - ) - }, - ) - } + } + if (isLikedFestivalDeleteDialogVisible) { + LikedFestivalDeleteDialog( + onCancelClick = { + onFestivalUiAction( + FestivalUiAction.OnDeleteDialogButtonClick(ButtonType.CANCEL), + ) + }, + onConfirmClick = { + onFestivalUiAction( + FestivalUiAction.OnDeleteDialogButtonClick(ButtonType.CONFIRM), + ) + }, + ) } } @ComponentPreview @Composable -fun SchoolSearchBottomSheetPreview() { +fun SchoolSearchBottomSheetPreview( + @PreviewParameter(FestivalPreviewParameterProvider::class) + festivalUiState: FestivalUiState, +) { UnifestTheme { FestivalSearchBottomSheet( - searchTextHintRes = R.string.festival_search_text_field_hint, + searchTextHintRes = designR.string.festival_search_text_field_hint, searchText = TextFieldValue(), - likedFestivals = persistentListOf( - FestivalModel( - 1, - 1, - "https://picsum.photos/36", - "서울대학교", - "서울", - "설대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 2, - 2, - "https://picsum.photos/36", - "연세대학교", - "서울", - "연대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 3, - 3, - "https://picsum.photos/36", - "고려대학교", - "서울", - "고대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 4, - 4, - "https://picsum.photos/36", - "성균관대학교", - "서울", - "성대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 5, - 5, - "https://picsum.photos/36", - "건국대학교", - "서울", - "건대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - ), - festivalSearchResults = persistentListOf( - FestivalModel( - 1, - 1, - "https://picsum.photos/36", - "서울대학교", - "서울", - "설대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 2, - 2, - "https://picsum.photos/36", - "연세대학교", - "서울", - "연대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 3, - 3, - "https://picsum.photos/36", - "고려대학교", - "서울", - "고대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 4, - 4, - "https://picsum.photos/36", - "성균관대학교", - "성대축제", - "서울", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 5, - 5, - "https://picsum.photos/36", - "건국대학교", - "서울", - "건대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - ), + likedFestivals = festivalUiState.festivals, + festivalSearchResults = festivalUiState.likedFestivals, isSearchMode = false, isEditMode = false, isLikedFestivalDeleteDialogVisible = false, diff --git a/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/FestivalSearchResults.kt b/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/FestivalSearchResults.kt index a21da805..64cbf8f7 100644 --- a/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/FestivalSearchResults.kt +++ b/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/FestivalSearchResults.kt @@ -1,5 +1,6 @@ package com.unifest.android.feature.festival +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -16,15 +17,16 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.unifest.android.core.common.extension.noRippleClickable import com.unifest.android.core.common.utils.formatToString @@ -38,9 +40,12 @@ import com.unifest.android.core.designsystem.theme.Content4 import com.unifest.android.core.designsystem.theme.Content6 import com.unifest.android.core.model.FestivalModel import com.unifest.android.feature.festival.viewmodel.FestivalUiAction -import com.unifest.android.core.designsystem.R +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.feature.festival.preview.FestivalPreviewParameterProvider +import com.unifest.android.feature.festival.viewmodel.FestivalUiState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import com.unifest.android.core.designsystem.R as designR @Composable fun FestivalSearchResults( @@ -58,7 +63,7 @@ fun FestivalSearchResults( ) { Text( text = stringResource(id = R.string.no_result), - color = Color(0xFF7E7E7E), + color = MaterialTheme.colorScheme.onSurfaceVariant, style = Content3, ) } @@ -71,13 +76,13 @@ fun FestivalSearchResults( Row { Text( text = stringResource(id = R.string.search_result), - color = Color(0xFFABABAB), + color = MaterialTheme.colorScheme.onSurfaceVariant, style = Content6, ) Spacer(modifier = Modifier.width(5.dp)) Text( text = "총 ${searchResults.size}개", - color = Color(0xFF191919), + color = MaterialTheme.colorScheme.onBackground, style = Content6, ) } @@ -95,7 +100,7 @@ fun FestivalSearchResults( if (index != searchResults.size - 1) { HorizontalDivider( thickness = 1.dp, - color = Color(0xFFDFDFDF), + color = MaterialTheme.colorScheme.outline, ) } } @@ -114,6 +119,7 @@ fun FestivalSearchResultItem( Row( modifier = Modifier .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) .padding(start = 8.dp, top = 9.dp, bottom = 14.dp, end = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -128,17 +134,19 @@ fun FestivalSearchResultItem( Column { Text( text = festival.schoolName, + color = MaterialTheme.colorScheme.onBackground, style = Content2, ) Spacer(modifier = Modifier.height(3.dp)) Text( text = festival.festivalName, + color = MaterialTheme.colorScheme.onBackground, style = Content4, ) Spacer(modifier = Modifier.height(3.dp)) Text( text = "${festival.beginDate.toLocalDate().formatToString()} - ${festival.endDate.toLocalDate().formatToString()}", - color = Color(0xFF4D4D4D), + color = MaterialTheme.colorScheme.onSurfaceVariant, style = Content3, ) } @@ -147,8 +155,8 @@ fun FestivalSearchResultItem( UnifestOutlinedButton( onClick = {}, cornerRadius = 17.dp, - borderColor = Color(0xFFDDDDDD), - contentColor = Color(0xFF666666), + borderColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.primary, contentPadding = PaddingValues(horizontal = 17.dp), enabled = false, modifier = Modifier @@ -159,17 +167,17 @@ fun FestivalSearchResultItem( .noRippleClickable {}, ) { Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_check), + imageVector = ImageVector.vectorResource(designR.drawable.ic_check), contentDescription = "Checked", - tint = Color(0xFF666666), + tint = MaterialTheme.colorScheme.primary, ) } } else { UnifestOutlinedButton( onClick = { onFestivalUiAction(FestivalUiAction.OnAddClick(festival)) }, cornerRadius = 17.dp, - borderColor = Color(0xFFDDDDDD), - contentColor = Color(0xFF666666), + borderColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.primary, contentPadding = PaddingValues(horizontal = 17.dp), modifier = Modifier.defaultMinSize( minWidth = ButtonDefaults.MinWidth, @@ -187,70 +195,14 @@ fun FestivalSearchResultItem( @ComponentPreview @Composable -fun FestivalSearchResultsPreview() { - FestivalSearchResults( - searchResults = persistentListOf( - FestivalModel( - 1, - 1, - "https://picsum.photos/36", - "서울대학교", - "서울", - "설대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 2, - 2, - "https://picsum.photos/36", - "연세대학교", - "서울", - "연대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 3, - 3, - "https://picsum.photos/36", - "고려대학교", - "서울", - "고대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 4, - 4, - "https://picsum.photos/36", - "성균관대학교", - "서울", - "성대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 5, - 5, - "https://picsum.photos/36", - "건국대학교", - "서울", - "건대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - ), - onFestivalUiAction = {}, - ) +fun FestivalSearchResultsPreview( + @PreviewParameter(FestivalPreviewParameterProvider::class) + festivalUiState: FestivalUiState, +) { + UnifestTheme { + FestivalSearchResults( + searchResults = festivalUiState.likedFestivals, + onFestivalUiAction = {}, + ) + } } diff --git a/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/preview/FestivalPreviewParameterProvider.kt b/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/preview/FestivalPreviewParameterProvider.kt new file mode 100644 index 00000000..f683d865 --- /dev/null +++ b/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/preview/FestivalPreviewParameterProvider.kt @@ -0,0 +1,137 @@ +package com.unifest.android.feature.festival.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.unifest.android.core.model.FestivalModel +import com.unifest.android.feature.festival.viewmodel.FestivalUiState +import kotlinx.collections.immutable.persistentListOf + +class FestivalPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + FestivalUiState( + festivals = persistentListOf( + FestivalModel( + 1, + 1, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 2, + 2, + "https://picsum.photos/36", + "연세대학교", + "서울", + "연대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 3, + 3, + "https://picsum.photos/36", + "고려대학교", + "서울", + "고대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 4, + 4, + "https://picsum.photos/36", + "성균관대학교", + "서울", + "성대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 5, + 5, + "https://picsum.photos/36", + "건국대학교", + "서울", + "건대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + ), + likedFestivals = persistentListOf( + FestivalModel( + 1, + 1, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 2, + 2, + "https://picsum.photos/36", + "연세대학교", + "서울", + "연대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 3, + 3, + "https://picsum.photos/36", + "고려대학교", + "서울", + "고대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 4, + 4, + "https://picsum.photos/36", + "성균관대학교", + "서울", + "성대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 5, + 5, + "https://picsum.photos/36", + "건국대학교", + "서울", + "건대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + ), + ), + ) +} diff --git a/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/viewmodel/FestivalViewModel.kt b/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/viewmodel/FestivalViewModel.kt index 95b00436..6acf57b1 100644 --- a/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/viewmodel/FestivalViewModel.kt +++ b/feature/festival/src/main/kotlin/com/unifest/android/feature/festival/viewmodel/FestivalViewModel.kt @@ -10,8 +10,9 @@ import com.unifest.android.core.common.utils.matchesSearchText import com.unifest.android.core.data.repository.FestivalRepository import com.unifest.android.core.data.repository.LikedFestivalRepository import com.unifest.android.core.data.repository.OnboardingRepository -import com.unifest.android.core.designsystem.R +import com.unifest.android.core.designsystem.R as designR import com.unifest.android.core.model.FestivalModel +import com.unifest.android.feature.festival.R import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -24,6 +25,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -127,14 +129,22 @@ class FestivalViewModel @Inject constructor( setFestivalSearchBottomSheetVisible(false) _uiEvent.send(FestivalUiEvent.NavigateBack) } else { - _uiEvent.send(FestivalUiEvent.ShowToast(UiText.StringResource(R.string.menu_interest_festival_snack_bar))) + _uiEvent.send(FestivalUiEvent.ShowToast(UiText.StringResource(designR.string.interest_festival_snack_bar))) } } } private fun addLikeFestival(festival: FestivalModel) { viewModelScope.launch { - likedFestivalRepository.insertLikedFestivalAtSearch(festival) + likedFestivalRepository.registerLikedFestival() + .onSuccess { + likedFestivalRepository.insertLikedFestivalAtSearch(festival) + _uiEvent.send(FestivalUiEvent.ShowToast(UiText.StringResource(R.string.liked_festival_saved_message))) + } + .onFailure { exception -> + _uiEvent.send(FestivalUiEvent.ShowToast(UiText.StringResource(R.string.liked_festival_saved_failed_message))) + Timber.e(exception) + } } } @@ -181,7 +191,15 @@ class FestivalViewModel @Inject constructor( private fun deleteLikedFestival(festival: FestivalModel) { viewModelScope.launch { - likedFestivalRepository.deleteLikedFestival(festival) + likedFestivalRepository.unregisterLikedFestival() + .onSuccess { + likedFestivalRepository.deleteLikedFestival(festival) + _uiEvent.send(FestivalUiEvent.ShowToast(UiText.StringResource(R.string.liked_festival_removed_message))) + } + .onFailure { exception -> + _uiEvent.send(FestivalUiEvent.ShowToast(UiText.StringResource(R.string.liked_festival_removed_failed_message))) + Timber.e(exception) + } } } diff --git a/feature/festival/src/main/res/values/strings.xml b/feature/festival/src/main/res/values/strings.xml new file mode 100644 index 00000000..ff4cb6ac --- /dev/null +++ b/feature/festival/src/main/res/values/strings.xml @@ -0,0 +1,14 @@ + + + 나의 관심 축제 + 편집 + 완료 + 추가 + 검색결과 + 검색 결과가 없어요 + 관심 축제로 저장했습니다. + 관심 축제로 저장을 실패했습니다. + 관심 축제에서 삭제했습니다. + 관심 축제에서 삭제를 실패했습니다. + + diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index 890093a0..d624900a 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.unifest.android.feature) + alias(libs.plugins.kotlin.serialization) // alias(libs.plugins.compose.investigator) } diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt index 78bde3a2..1f1516b7 100644 --- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt @@ -1,80 +1,62 @@ package com.unifest.android.feature.home import android.widget.Toast -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme 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.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.unifest.android.core.common.ObserveAsEvents import com.unifest.android.core.common.UiText -import com.unifest.android.core.common.utils.formatWithDayOfWeek -import com.unifest.android.core.common.utils.toLocalDate -import com.unifest.android.core.designsystem.R -import com.unifest.android.core.designsystem.component.NetworkImage -import com.unifest.android.core.designsystem.component.UnifestHorizontalDivider import com.unifest.android.core.designsystem.component.UnifestOutlinedButton import com.unifest.android.core.designsystem.theme.BoothLocation -import com.unifest.android.core.designsystem.theme.Content4 -import com.unifest.android.core.designsystem.theme.Content5 import com.unifest.android.core.designsystem.theme.Content6 import com.unifest.android.core.designsystem.theme.Title2 import com.unifest.android.core.designsystem.theme.Title3 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.android.core.model.FestivalModel -import com.unifest.android.core.model.FestivalTodayModel import com.unifest.android.core.ui.DevicePreview -import com.unifest.android.core.ui.component.StarImage import com.unifest.android.feature.festival.FestivalSearchBottomSheet import com.unifest.android.feature.festival.viewmodel.FestivalUiAction import com.unifest.android.feature.festival.viewmodel.FestivalUiEvent import com.unifest.android.feature.festival.viewmodel.FestivalUiState import com.unifest.android.feature.festival.viewmodel.FestivalViewModel +import com.unifest.android.feature.home.component.Calendar +import com.unifest.android.feature.home.component.FestivalScheduleItem +import com.unifest.android.feature.home.component.IncomingFestivalCard +import com.unifest.android.feature.home.preview.HomePreviewParameterProvider import com.unifest.android.feature.home.viewmodel.HomeUiAction import com.unifest.android.feature.home.viewmodel.HomeUiEvent import com.unifest.android.feature.home.viewmodel.HomeUiState import com.unifest.android.feature.home.viewmodel.HomeViewModel -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import java.time.LocalDate import java.time.format.DateTimeFormatter -import java.time.temporal.ChronoUnit +import com.unifest.android.core.designsystem.R as designR +// TODO 이미 관심 축제로 추가된 축제는 관심 축제로 추가하기 버튼이 보이면 안됨 @Composable internal fun HomeRoute( padding: PaddingValues, @@ -122,6 +104,7 @@ internal fun HomeScreen( Box( modifier = Modifier .fillMaxSize() + .background(MaterialTheme.colorScheme.background) .padding(padding), ) { LazyColumn { @@ -135,7 +118,13 @@ internal fun HomeScreen( ) } item { - FestivalScheduleText(selectedDate = homeUiState.selectedDate) + Text( + text = DateTimeFormatter.ofPattern("M월 d일") + .format(homeUiState.selectedDate) + stringResource(id = R.string.home_festival_schedule_text), + modifier = Modifier.padding(start = 20.dp, top = 20.dp), + color = MaterialTheme.colorScheme.onBackground, + style = Title3, + ) } if (homeUiState.todayFestivals.isEmpty()) { item { @@ -146,23 +135,25 @@ internal fun HomeScreen( .padding(64.dp), ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Image( + Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_schedule), contentDescription = "축제 없음", modifier = Modifier.size(23.dp), + tint = MaterialTheme.colorScheme.onBackground, ) Spacer(modifier = Modifier.height(10.dp)) Text( text = stringResource(id = R.string.home_empty_festival_text), - style = Title2, + color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Center, + style = Title2, ) Spacer(modifier = Modifier.height(9.dp)) Text( text = stringResource(id = R.string.home_empty_festival_schedule_description_text), - style = Content6, + color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, - color = Color(0xFF848484), + style = Content6, ) } } @@ -177,7 +168,7 @@ internal fun HomeScreen( FestivalScheduleItem( festival = festival, scheduleIndex = scheduleIndex, - likedFestivals = homeUiState.likedFestivals, +// likedFestivals = homeUiState.likedFestivals, selectedDate = homeUiState.selectedDate, isDataReady = homeUiState.isDataReady, isStarImageClicked = homeUiState.isStarImageClicked[scheduleIndex], @@ -187,8 +178,8 @@ internal fun HomeScreen( if (scheduleIndex < homeUiState.todayFestivals.size - 1) { Spacer(modifier = Modifier.height(16.dp)) HorizontalDivider( - color = Color(0xFFDFDFDF), modifier = Modifier.padding(horizontal = 20.dp), + color = MaterialTheme.colorScheme.outline, ) } } @@ -199,8 +190,8 @@ internal fun HomeScreen( modifier = Modifier .fillMaxWidth() .padding(20.dp), - contentColor = Color(0xFF585858), - borderColor = Color(0xFFD2D2D2), + contentColor = MaterialTheme.colorScheme.onSecondary, + borderColor = MaterialTheme.colorScheme.secondaryContainer, ) { Text( text = stringResource(id = R.string.home_add_interest_festival_button), @@ -208,12 +199,24 @@ internal fun HomeScreen( ) } } - item { UnifestHorizontalDivider() } + item { + HorizontalDivider( + thickness = 8.dp, + color = MaterialTheme.colorScheme.outline, + ) + } item { Spacer(modifier = Modifier.height(20.dp)) } - item { IncomingFestivalText() } + item { + Text( + text = stringResource(id = R.string.home_incoming_festival_text), + modifier = Modifier.padding(start = 20.dp, bottom = 16.dp), + color = MaterialTheme.colorScheme.onBackground, + style = Title3, + ) + } if (homeUiState.incomingFestivals.isNotEmpty()) { items(homeUiState.incomingFestivals) { festival -> - IncomingFestivalCard(festival) + IncomingFestivalCard(festival = festival) Spacer(modifier = Modifier.height(8.dp)) } } @@ -221,7 +224,7 @@ internal fun HomeScreen( if (festivalUiState.isFestivalSearchBottomSheetVisible) { FestivalSearchBottomSheet( searchText = festivalUiState.festivalSearchText, - searchTextHintRes = R.string.festival_search_text_field_hint, + searchTextHintRes = designR.string.festival_search_text_field_hint, likedFestivals = festivalUiState.likedFestivals, festivalSearchResults = festivalUiState.festivalSearchResults, isSearchMode = festivalUiState.isSearchMode, @@ -239,264 +242,16 @@ internal fun HomeScreen( } } -@Composable -fun FestivalScheduleText(selectedDate: LocalDate) { - val formattedDate = DateTimeFormatter.ofPattern("M월 d일").format(selectedDate) - Text( - text = formattedDate + stringResource(id = R.string.home_festival_schedule_text), - style = Title3, - modifier = Modifier.padding(start = 20.dp, top = 20.dp), - ) -} - -@Composable -fun FestivalScheduleItem( - festival: FestivalTodayModel, - scheduleIndex: Int, - likedFestivals: ImmutableList, - selectedDate: LocalDate, - isStarImageClicked: ImmutableList, - isDataReady: Boolean, - onHomeUiAction: (HomeUiAction) -> Unit, -) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 20.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier - .width(3.dp) - .height(72.dp) - .background(Color(0xFF1FC0BA)) - .align(Alignment.CenterVertically), - ) - Spacer(modifier = Modifier.width(8.dp)) - Column( - modifier = Modifier.width(172.dp), - ) { - Text( - text = "${festival.beginDate.toLocalDate().formatWithDayOfWeek()} - ${festival.endDate.toLocalDate().formatWithDayOfWeek()}", - style = Content4, - color = Color(0xFFC0C0C0), - ) - Spacer(modifier = Modifier.height(5.dp)) - Row { - Text( - text = festival.festivalName + " Day ", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = Title2, - ) - if (isDataReady) { - Text( - text = (ChronoUnit.DAYS.between(festival.beginDate.toLocalDate(), selectedDate) + 1).toString(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = Title2, - ) - } - } - Spacer(modifier = Modifier.height(7.dp)) - Row { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_location_gray), - contentDescription = "Location Icon", - modifier = Modifier - .size(10.dp) - .align(Alignment.CenterVertically), - tint = Color.Unspecified, - ) - Spacer(modifier = Modifier.width(5.dp)) - Text( - text = festival.schoolName, - style = Content5, - color = Color(0xFF848484), - ) - } - } - if (festival.starInfo.isNotEmpty()) { - LazyRow { - itemsIndexed( - items = festival.starInfo, - key = { _, starInfo -> starInfo.starId }, - ) { starIndex, starInfo -> - StarImage( - imgUrl = starInfo.imgUrl, - onClick = { - onHomeUiAction(HomeUiAction.OnToggleStarImageClick(scheduleIndex, starIndex, !isStarImageClicked[starIndex])) - }, - onLongClick = { - onHomeUiAction(HomeUiAction.OnStarImageLongClick(scheduleIndex, starIndex)) - }, - isClicked = isStarImageClicked[starIndex], - label = starInfo.name, - modifier = Modifier - .size(72.dp) - .clip(CircleShape), - ) - Spacer(modifier = Modifier.width(10.dp)) - } - } - } - } - if (!likedFestivals.any { it.festivalId == festival.festivalId }) { - UnifestOutlinedButton( - onClick = { - onHomeUiAction(HomeUiAction.OnAddAsLikedFestivalClick(festival)) - }, - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .padding(top = 16.dp, start = 20.dp, end = 20.dp), - contentPadding = PaddingValues(6.dp), - ) { - Text( - text = stringResource(id = R.string.home_add_interest_festival_in_item_button), - style = BoothLocation, - ) - } - } - } -} - -@Composable -fun IncomingFestivalText() { - Text( - text = stringResource(id = R.string.home_incoming_festival_text), - modifier = Modifier.padding(start = 20.dp, bottom = 16.dp), - style = Title3, - ) -} - -@Composable -fun IncomingFestivalCard(festival: FestivalModel) { - Card( - modifier = Modifier - .padding(horizontal = 20.dp) - .fillMaxWidth(), - shape = RoundedCornerShape(10.dp), - colors = CardDefaults.cardColors(containerColor = Color.White), - border = BorderStroke(1.dp, Color(0xFFDEDEDE)), - ) { - Row( - modifier = Modifier - .padding(20.dp) - .fillMaxSize(), - verticalAlignment = Alignment.CenterVertically, - ) { - NetworkImage( - imgUrl = festival.thumbnail, - contentDescription = "Festival Thumbnail", - modifier = Modifier - .size(52.dp) - .clip(CircleShape), - ) - Spacer(modifier = Modifier.width(10.dp)) - Column( - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = "${festival.beginDate.toLocalDate().formatWithDayOfWeek()} - ${festival.endDate.toLocalDate().formatWithDayOfWeek()}", - style = Content6, - color = Color(0xFF848484), - ) - Spacer(modifier = Modifier.height(5.dp)) - Text( - text = festival.festivalName, - style = Content4, - ) - Spacer(modifier = Modifier.height(5.dp)) - Row { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_location_gray), - contentDescription = "Location Icon", - modifier = Modifier - .size(10.dp) - .align(Alignment.CenterVertically), - tint = Color.Unspecified, - ) - Spacer(modifier = Modifier.width(5.dp)) - Text( - text = festival.schoolName, - style = Content6, - color = Color(0xFF848484), - ) - } - } - } - } -} - @DevicePreview @Composable -fun HomeScreenPreview() { +private fun HomeScreenPreview( + @PreviewParameter(HomePreviewParameterProvider::class) + homeUiState: HomeUiState, +) { UnifestTheme { HomeScreen( padding = PaddingValues(), - homeUiState = HomeUiState( - todayFestivals = persistentListOf( - FestivalTodayModel( - festivalId = 1, - beginDate = "2024-04-05", - endDate = "2024-04-07", - festivalName = "녹색지대 DAY 1", - schoolName = "건국대학교 서울캠퍼스", - starInfo = listOf(), - thumbnail = "https://picsum.photos/36", - schoolId = 1, - ), - - FestivalTodayModel( - festivalId = 2, - beginDate = "2024-04-05", - endDate = "2024-04-07", - festivalName = "녹색지대 DAY 1", - schoolName = "건국대학교 서울캠퍼스", - starInfo = listOf(), - thumbnail = "https://picsum.photos/36", - schoolId = 2, - ), - FestivalTodayModel( - festivalId = 3, - beginDate = "2024-04-05", - endDate = "2024-04-07", - festivalName = "녹색지대 DAY 1", - schoolName = "건국대학교 서울캠퍼스", - starInfo = listOf(), - thumbnail = "https://picsum.photos/36", - schoolId = 3, - ), - ), - incomingFestivals = persistentListOf( - FestivalModel( - 1, - 1, - "https://picsum.photos/36", - "서울대학교", - "서울", - "설대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 2, - 2, - "https://picsum.photos/36", - "연세대학교", - "서울", - "연대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - ), - ), + homeUiState = homeUiState, festivalUiState = FestivalUiState(), onHomeUiAction = {}, onFestivalUiAction = {}, diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/StarImageDialog.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/StarImageDialog.kt index cbabe22f..ae890ccb 100644 --- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/StarImageDialog.kt +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/StarImageDialog.kt @@ -22,8 +22,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.min import androidx.compose.ui.window.DialogProperties +import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.component.NetworkImage import com.unifest.android.core.designsystem.theme.Title2 +import com.unifest.android.core.designsystem.theme.UnifestTheme import com.unifest.android.core.model.StarInfoModel @OptIn(ExperimentalMaterial3Api::class) @@ -71,3 +73,18 @@ fun StarImageDialog( } } } + +@ComponentPreview +@Composable +private fun StarImageDialogPreview() { + UnifestTheme { + StarImageDialog( + onDismissRequest = {}, + star = StarInfoModel( + starId = 0L, + name = "창모", + imgUrl = "", + ), + ) + } +} diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/component/Calendar.kt similarity index 84% rename from feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt rename to feature/home/src/main/kotlin/com/unifest/android/feature/home/component/Calendar.kt index ee1a3a9f..e3a5f8b2 100644 --- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/component/Calendar.kt @@ -1,9 +1,10 @@ -package com.unifest.android.feature.home +package com.unifest.android.feature.home.component import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -20,6 +21,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -32,13 +34,11 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.kizitonwose.calendar.compose.CalendarState import com.kizitonwose.calendar.compose.HorizontalCalendar @@ -54,13 +54,22 @@ import com.kizitonwose.calendar.core.nextMonth import com.kizitonwose.calendar.core.previousMonth import com.kizitonwose.calendar.core.yearMonth import com.unifest.android.core.common.utils.toLocalDate -import com.unifest.android.core.designsystem.R +import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.theme.BoothTitle0 import com.unifest.android.core.designsystem.theme.Content6 +import com.unifest.android.core.designsystem.theme.DarkBlueGreen +import com.unifest.android.core.designsystem.theme.DarkOrange +import com.unifest.android.core.designsystem.theme.DarkRed +import com.unifest.android.core.designsystem.theme.LightBlueGreen +import com.unifest.android.core.designsystem.theme.LightOrange +import com.unifest.android.core.designsystem.theme.LightRed import com.unifest.android.core.designsystem.theme.Title5 -import com.unifest.android.core.designsystem.theme.MainColor import com.unifest.android.core.designsystem.theme.UnifestTheme import com.unifest.android.core.model.FestivalModel +import com.unifest.android.feature.home.R +import com.unifest.android.feature.home.clickable +import com.unifest.android.feature.home.displayText +import com.unifest.android.feature.home.rememberFirstVisibleMonthAfterScroll import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -87,7 +96,7 @@ fun Calendar( modifier = Modifier.shadow(4.dp, RoundedCornerShape(bottomEnd = 20.dp, bottomStart = 20.dp)), ) { Column( - modifier = Modifier.background(Color.White), + modifier = Modifier.background(MaterialTheme.colorScheme.surface), ) { val monthState = rememberCalendarState( startMonth = startMonth, @@ -163,7 +172,7 @@ fun ModeToggleButton( .fillMaxWidth() .requiredHeight(40.dp) .paint( - painter = painterResource(id = R.drawable.calender_bottom), + painter = painterResource(id = R.drawable.ic_calender_bottom), contentScale = ContentScale.FillBounds, ) .then(modifier), @@ -173,10 +182,13 @@ fun ModeToggleButton( onClick = { onModeChange(!isWeekMode) }, ) { Icon( - imageVector = if (isWeekMode) ImageVector.vectorResource(id = R.drawable.ic_calender_down) - else ImageVector.vectorResource(id = R.drawable.ic_calender_up), + imageVector = if (isWeekMode) { + ImageVector.vectorResource(id = R.drawable.ic_calender_down) + } else { + ImageVector.vectorResource(id = R.drawable.ic_calender_up) + }, contentDescription = if (isWeekMode) "Month" else "Week", - tint = Color(0xFFD9D9D9), + tint = MaterialTheme.colorScheme.onSecondaryContainer, ) } } @@ -203,7 +215,7 @@ private fun CalendarNavigationIcon( .align(Alignment.Center), imageVector = icon, contentDescription = contentDescription, - tint = Color.Gray, + tint = MaterialTheme.colorScheme.onSecondary, ) } @@ -249,9 +261,9 @@ fun MonthAndWeekCalendarTitle( } } +// 실제로 달력의 상단에 현재 월을 표시하고, 이전/다음 월로 이동할 수 있는 화살표 아이콘을 제공하는 UI 컴포넌트 @Composable fun SimpleCalendarTitle( - // 실제로 달력의 상단에 현재 월을 표시하고, 이전/다음 월로 이동할 수 있는 화살표 아이콘을 제공하는 UI 컴포넌트 currentMonth: Month, currentYear: Int, goToPrevious: () -> Unit, @@ -271,8 +283,9 @@ fun SimpleCalendarTitle( ) { Text( text = "${currentYear}년 ${currentMonth.displayText()}", - style = BoothTitle0, + color = MaterialTheme.colorScheme.onBackground, textAlign = TextAlign.Start, + style = BoothTitle0, ) Spacer(modifier = Modifier.width(6.dp)) ColorCircleWithText(color = Color(0xFF1FC0BA), text = "1개") @@ -310,9 +323,10 @@ fun ColorCircleWithText(color: Color, text: String) { Spacer(modifier = Modifier.width(4.dp)) Text( text = text, - style = Content6, + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, + style = Content6, ) } } @@ -325,11 +339,11 @@ fun CalendarHeader(daysOfWeek: ImmutableList) { ) { for (dayOfWeek in daysOfWeek) { Text( + text = dayOfWeek.displayText(), modifier = Modifier.weight(1f), + color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center, - text = dayOfWeek.displayText(), style = Content6, - color = Color.Gray, ) } } @@ -361,22 +375,22 @@ fun Day( ) { Box( modifier = Modifier - .aspectRatio(1f) // This is important for square-sizing! + .aspectRatio(1f) .padding(16.dp) .clip(CircleShape) - .background(color = if (isSelected) MainColor else Color.Transparent) + .background(color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent) .then( if (day == currentDate) { - Modifier.border(2.dp, MainColor, CircleShape) + Modifier.border(2.dp, MaterialTheme.colorScheme.primary, CircleShape) } else Modifier, ), contentAlignment = Alignment.Center, ) { val textColor = when { isSelected -> Color.White - isToday -> MainColor - isSelectable -> Color.Unspecified - else -> colorResource(R.color.inactive_text_color) + isToday -> MaterialTheme.colorScheme.primary + isSelectable -> MaterialTheme.colorScheme.onBackground + else -> MaterialTheme.colorScheme.onSecondaryContainer } Text( text = day.dayOfMonth.toString(), @@ -386,9 +400,9 @@ fun Day( } if (festivalCount > 0) { val festivalDotColor = when (festivalCount) { - 1 -> Color(0xFF1FC0BA) - 2 -> Color(0xFFFF8A1F) - else -> Color(0xFFFF3939) + 1 -> if (isSystemInDarkTheme()) DarkBlueGreen else LightBlueGreen + 2 -> if (isSystemInDarkTheme()) DarkOrange else LightOrange + else -> if (isSystemInDarkTheme()) DarkRed else LightRed } Box( modifier = Modifier @@ -401,9 +415,9 @@ fun Day( } } -@Preview +@ComponentPreview @Composable -private fun CalendarPreview() { +fun CalendarPreview() { UnifestTheme { Calendar( selectedDate = LocalDate.now(), diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/component/FestivalScheduleItem.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/component/FestivalScheduleItem.kt new file mode 100644 index 00000000..25204d6c --- /dev/null +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/component/FestivalScheduleItem.kt @@ -0,0 +1,186 @@ +package com.unifest.android.feature.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.unifest.android.core.common.utils.formatWithDayOfWeek +import com.unifest.android.core.common.utils.toLocalDate +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.core.designsystem.theme.Content4 +import com.unifest.android.core.designsystem.theme.Content5 +import com.unifest.android.core.designsystem.theme.DarkBlueGreen +import com.unifest.android.core.designsystem.theme.LightBlueGreen +import com.unifest.android.core.designsystem.theme.Title2 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.model.FestivalTodayModel +import com.unifest.android.core.ui.component.StarImage +import com.unifest.android.feature.home.viewmodel.HomeUiAction +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +@Composable +fun FestivalScheduleItem( + festival: FestivalTodayModel, + scheduleIndex: Int, +// likedFestivals: ImmutableList, + selectedDate: LocalDate, + isStarImageClicked: ImmutableList, + isDataReady: Boolean, + onHomeUiAction: (HomeUiAction) -> Unit, + modifier: Modifier = Modifier, +) { + Column { + Row( + modifier = modifier + .fillMaxWidth() + .padding(start = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .width(3.dp) + .height(72.dp) + .background(if (isSystemInDarkTheme()) DarkBlueGreen else LightBlueGreen) + .align(Alignment.CenterVertically), + ) + Spacer(modifier = Modifier.width(8.dp)) + Column( + modifier = Modifier.width(172.dp), + ) { + Text( + text = "${festival.beginDate.toLocalDate().formatWithDayOfWeek()} - ${festival.endDate.toLocalDate().formatWithDayOfWeek()}", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = Content4, + ) + Spacer(modifier = Modifier.height(5.dp)) + Row { + Text( + text = festival.festivalName + " Day ", + color = MaterialTheme.colorScheme.onBackground, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = Title2, + ) + if (isDataReady) { + Text( + text = (ChronoUnit.DAYS.between(festival.beginDate.toLocalDate(), selectedDate) + 1).toString(), + color = MaterialTheme.colorScheme.onBackground, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = Title2, + ) + } + } + Spacer(modifier = Modifier.height(7.dp)) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_location_grey), + contentDescription = "Location Icon", + modifier = Modifier + .size(10.dp) + .align(Alignment.CenterVertically), + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = festival.schoolName, + style = Content5, + color = MaterialTheme.colorScheme.onSecondary, + ) + } + } + if (festival.starInfo.isNotEmpty()) { + LazyRow { + itemsIndexed( + items = festival.starInfo, + key = { _, starInfo -> starInfo.starId }, + ) { starIndex, starInfo -> + StarImage( + imgUrl = starInfo.imgUrl, + onClick = { + onHomeUiAction(HomeUiAction.OnToggleStarImageClick(scheduleIndex, starIndex, !isStarImageClicked[starIndex])) + }, + onLongClick = { + onHomeUiAction(HomeUiAction.OnStarImageLongClick(scheduleIndex, starIndex)) + }, + isClicked = isStarImageClicked[starIndex], + label = starInfo.name, + modifier = Modifier + .size(72.dp) + .clip(CircleShape), + ) + Spacer(modifier = Modifier.width(10.dp)) + } + } + } + } +// if (!likedFestivals.any { it.festivalId == festival.festivalId }) { +// UnifestOutlinedButton( +// onClick = { +// onHomeUiAction(HomeUiAction.OnAddAsLikedFestivalClick(festival)) +// }, +// modifier = Modifier +// .fillMaxWidth() +// .height(48.dp) +// .padding(top = 16.dp, start = 20.dp, end = 20.dp), +// contentPadding = PaddingValues(6.dp), +// ) { +// Text( +// text = stringResource(id = R.string.home_add_interest_festival_in_item_button), +// style = BoothLocation, +// ) +// } +// } + } +} + +@ComponentPreview +@Composable +fun FestivalScheduleItemPreview() { + UnifestTheme { + FestivalScheduleItem( + festival = FestivalTodayModel( + festivalId = 1, + schoolId = 1, + festivalName = "대동제", + schoolName = "건국대", + beginDate = "2024-05-21", + endDate = "2024-05-23", + starInfo = emptyList(), + thumbnail = "", + ), + scheduleIndex = 0, +// likedFestivals = persistentListOf(), + selectedDate = LocalDate.now(), + isStarImageClicked = persistentListOf(), + isDataReady = true, + onHomeUiAction = {}, + ) + } +} diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/component/IncomingFestivalCard.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/component/IncomingFestivalCard.kt new file mode 100644 index 00000000..d218ff3a --- /dev/null +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/component/IncomingFestivalCard.kt @@ -0,0 +1,114 @@ +package com.unifest.android.feature.home.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.unifest.android.core.common.utils.formatWithDayOfWeek +import com.unifest.android.core.common.utils.toLocalDate +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.core.designsystem.component.NetworkImage +import com.unifest.android.core.designsystem.theme.Content4 +import com.unifest.android.core.designsystem.theme.Content6 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.model.FestivalModel + +@Composable +fun IncomingFestivalCard( + festival: FestivalModel, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + shape = RoundedCornerShape(10.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + ) { + Row( + modifier = Modifier.padding(20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + NetworkImage( + imgUrl = festival.thumbnail, + contentDescription = "Festival Thumbnail", + modifier = Modifier + .size(52.dp) + .clip(CircleShape), + ) + Spacer(modifier = Modifier.width(10.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = "${festival.beginDate.toLocalDate().formatWithDayOfWeek()} - ${festival.endDate.toLocalDate().formatWithDayOfWeek()}", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = Content6, + ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = festival.festivalName, + color = MaterialTheme.colorScheme.onBackground, + style = Content4, + ) + Spacer(modifier = Modifier.height(5.dp)) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_location_grey), + contentDescription = "Location Icon", + modifier = Modifier + .size(10.dp) + .align(Alignment.CenterVertically), + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = festival.schoolName, + style = Content6, + color = MaterialTheme.colorScheme.onSecondary, + ) + } + } + } + } +} + +@ComponentPreview +@Composable +fun IncomingFestivalCardPreview() { + UnifestTheme { + IncomingFestivalCard( + festival = FestivalModel( + festivalId = 1, + schoolId = 1, + festivalName = "대동제", + beginDate = "2024-05-21", + endDate = "2024-05-23", + thumbnail = "", + schoolName = "건국대", + ), + ) + } +} diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/navigation/HomeNavigation.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/navigation/HomeNavigation.kt index 8971e626..2e85ed0d 100644 --- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/navigation/HomeNavigation.kt +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/navigation/HomeNavigation.kt @@ -6,12 +6,11 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.unifest.android.core.common.UiText +import com.unifest.android.core.navigation.MainTabRoute import com.unifest.android.feature.home.HomeRoute -const val HOME_ROUTE = "home_route" - fun NavController.navigateToHome(navOptions: NavOptions) { - navigate(HOME_ROUTE, navOptions) + navigate(MainTabRoute.Home, navOptions) } fun NavGraphBuilder.homeNavGraph( @@ -19,7 +18,7 @@ fun NavGraphBuilder.homeNavGraph( popBackStack: () -> Unit, onShowSnackBar: (UiText) -> Unit, ) { - composable(route = HOME_ROUTE) { + composable { HomeRoute( padding = padding, popBackStack = popBackStack, diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/preview/HomePreviewParameterProvider.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/preview/HomePreviewParameterProvider.kt new file mode 100644 index 00000000..7528341d --- /dev/null +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/preview/HomePreviewParameterProvider.kt @@ -0,0 +1,72 @@ +package com.unifest.android.feature.home.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.unifest.android.core.model.FestivalModel +import com.unifest.android.core.model.FestivalTodayModel +import com.unifest.android.feature.home.viewmodel.HomeUiState +import kotlinx.collections.immutable.persistentListOf + +internal class HomePreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + HomeUiState( + todayFestivals = persistentListOf( + FestivalTodayModel( + festivalId = 1, + beginDate = "2024-04-05", + endDate = "2024-04-07", + festivalName = "녹색지대 DAY 1", + schoolName = "건국대학교 서울캠퍼스", + starInfo = listOf(), + thumbnail = "https://picsum.photos/36", + schoolId = 1, + ), + FestivalTodayModel( + festivalId = 2, + beginDate = "2024-04-05", + endDate = "2024-04-07", + festivalName = "녹색지대 DAY 1", + schoolName = "건국대학교 서울캠퍼스", + starInfo = listOf(), + thumbnail = "https://picsum.photos/36", + schoolId = 2, + ), + FestivalTodayModel( + festivalId = 3, + beginDate = "2024-04-05", + endDate = "2024-04-07", + festivalName = "녹색지대 DAY 1", + schoolName = "건국대학교 서울캠퍼스", + starInfo = listOf(), + thumbnail = "https://picsum.photos/36", + schoolId = 3, + ), + ), + incomingFestivals = persistentListOf( + FestivalModel( + 1, + 1, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 2, + 2, + "https://picsum.photos/36", + "연세대학교", + "서울", + "연대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + ), + ), + ) +} diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt index 064f6a09..e10fd208 100644 --- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt @@ -7,8 +7,8 @@ import com.unifest.android.core.common.UiText import com.unifest.android.core.common.handleException import com.unifest.android.core.data.repository.FestivalRepository import com.unifest.android.core.data.repository.LikedFestivalRepository -import com.unifest.android.core.designsystem.R import com.unifest.android.core.model.FestivalTodayModel +import com.unifest.android.feature.home.R import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -60,8 +60,14 @@ class HomeViewModel @Inject constructor( private fun addLikeFestival(festival: FestivalTodayModel) { viewModelScope.launch { - likedFestivalRepository.insertLikedFestivalAtHome(festival) - _uiEvent.send(HomeUiEvent.ShowSnackBar(UiText.StringResource(R.string.home_add_interest_festival_snack_bar))) + likedFestivalRepository.registerLikedFestival() + .onSuccess { + likedFestivalRepository.insertLikedFestivalAtHome(festival) + _uiEvent.send(HomeUiEvent.ShowSnackBar(UiText.StringResource(R.string.home_add_interest_festival_saved_message))) + } + .onFailure { exception -> + _uiEvent.send(HomeUiEvent.ShowSnackBar(UiText.StringResource(R.string.home_add_interest_festival_saved_failed_message))) + } } } diff --git a/feature/home/src/main/res/drawable-night/ic_calender_bottom.xml b/feature/home/src/main/res/drawable-night/ic_calender_bottom.xml new file mode 100644 index 00000000..e9075738 --- /dev/null +++ b/feature/home/src/main/res/drawable-night/ic_calender_bottom.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/calender_bottom.xml b/feature/home/src/main/res/drawable/ic_calender_bottom.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/calender_bottom.xml rename to feature/home/src/main/res/drawable/ic_calender_bottom.xml diff --git a/core/designsystem/src/main/res/drawable/ic_calender_down.xml b/feature/home/src/main/res/drawable/ic_calender_down.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_calender_down.xml rename to feature/home/src/main/res/drawable/ic_calender_down.xml diff --git a/core/designsystem/src/main/res/drawable/ic_calender_left.xml b/feature/home/src/main/res/drawable/ic_calender_left.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_calender_left.xml rename to feature/home/src/main/res/drawable/ic_calender_left.xml diff --git a/core/designsystem/src/main/res/drawable/ic_calender_right.xml b/feature/home/src/main/res/drawable/ic_calender_right.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_calender_right.xml rename to feature/home/src/main/res/drawable/ic_calender_right.xml diff --git a/core/designsystem/src/main/res/drawable/ic_calender_up.xml b/feature/home/src/main/res/drawable/ic_calender_up.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_calender_up.xml rename to feature/home/src/main/res/drawable/ic_calender_up.xml diff --git a/core/designsystem/src/main/res/drawable/ic_schedule.xml b/feature/home/src/main/res/drawable/ic_schedule.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_schedule.xml rename to feature/home/src/main/res/drawable/ic_schedule.xml diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml new file mode 100644 index 00000000..e192e1f7 --- /dev/null +++ b/feature/home/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + + 의 축제 일정 + 오늘은 축제가 열리는 학교가 없어요 + 다가오는 축제 일정 + 관심 축제 추가하기 + 관심 축제로 추가 + 관심 축제로 저장했습니다 + 관심 축제로 저장을 실패했습니다. + 축제 일정 없음 + 오늘은 축제가 열리는 학교가 없어요 + + diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts index 6693b848..7974e077 100644 --- a/feature/intro/build.gradle.kts +++ b/feature/intro/build.gradle.kts @@ -15,6 +15,17 @@ android { defaultConfig { buildConfigField("String", "PACKAGE_NAME", "\"${libs.versions.packageName.get()}\"") } + + android { + buildTypes { + debug { + isMinifyEnabled = false + } + release { + isMinifyEnabled = false + } + } + } } dependencies { diff --git a/feature/intro/proguard-rules.pro b/feature/intro/proguard-rules.pro deleted file mode 100644 index 8dcc9ec0..00000000 --- a/feature/intro/proguard-rules.pro +++ /dev/null @@ -1,23 +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 - --dontwarn java.lang.invoke.StringConcatFactory diff --git a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/IntroActivity.kt b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/IntroActivity.kt index f769a1c7..1b0741a1 100644 --- a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/IntroActivity.kt +++ b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/IntroActivity.kt @@ -3,11 +3,13 @@ package com.unifest.android.feature.intro import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.graphics.Color import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import com.unifest.android.core.designsystem.theme.DarkGrey100 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.feature.navigator.MainNavigator +import com.unifest.android.feature.navigator.MainNavigator import dagger.hilt.android.AndroidEntryPoint import tech.thdev.compose.exteions.system.ui.controller.rememberExSystemUiController import javax.inject.Inject @@ -22,11 +24,12 @@ class IntroActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { val systemUiController = rememberExSystemUiController() + val isDarkTheme = isSystemInDarkTheme() DisposableEffect(systemUiController) { systemUiController.setSystemBarsColor( - color = Color.White, - darkIcons = true, + color = if (isDarkTheme) DarkGrey100 else Color.White, + darkIcons = !isDarkTheme, isNavigationBarContrastEnforced = false, ) diff --git a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/IntroScreen.kt b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/IntroScreen.kt index 039b74b3..0821fe8b 100644 --- a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/IntroScreen.kt +++ b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/IntroScreen.kt @@ -1,90 +1,52 @@ package com.unifest.android.feature.intro -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.ScrollableTabRow -import androidx.compose.material3.Tab +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.unifest.android.core.common.ObserveAsEvents -import com.unifest.android.core.common.utils.formatToString -import com.unifest.android.core.common.utils.toLocalDate -import com.unifest.android.core.designsystem.R import com.unifest.android.core.designsystem.component.LoadingWheel import com.unifest.android.core.designsystem.component.NetworkErrorDialog -import com.unifest.android.core.designsystem.component.NetworkImage import com.unifest.android.core.designsystem.component.SearchTextField import com.unifest.android.core.designsystem.component.ServerErrorDialog import com.unifest.android.core.designsystem.component.UnifestButton -import com.unifest.android.core.designsystem.component.UnifestHorizontalDivider import com.unifest.android.core.designsystem.component.UnifestScaffold import com.unifest.android.core.designsystem.theme.BoothLocation -import com.unifest.android.core.designsystem.theme.Content1 -import com.unifest.android.core.designsystem.theme.Content2 -import com.unifest.android.core.designsystem.theme.Content3 -import com.unifest.android.core.designsystem.theme.Content4 -import com.unifest.android.core.designsystem.theme.Content6 -import com.unifest.android.core.designsystem.theme.MainColor import com.unifest.android.core.designsystem.theme.Title2 -import com.unifest.android.core.designsystem.theme.Title3 import com.unifest.android.core.designsystem.theme.Title4 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.android.core.model.FestivalModel import com.unifest.android.core.ui.DevicePreview -import com.unifest.android.core.ui.component.FestivalItem +import com.unifest.android.feature.intro.component.AllFestivalsTabRow +import com.unifest.android.feature.intro.component.LikedFestivalsRow +import com.unifest.android.feature.intro.preview.IntroPreviewParameterProvider import com.unifest.android.feature.intro.viewmodel.ErrorType import com.unifest.android.feature.intro.viewmodel.IntroUiAction import com.unifest.android.feature.intro.viewmodel.IntroUiEvent import com.unifest.android.feature.intro.viewmodel.IntroUiState import com.unifest.android.feature.intro.viewmodel.IntroViewModel -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.launch +import com.unifest.android.core.designsystem.R as designR @Composable internal fun IntroRoute( @@ -113,7 +75,7 @@ fun IntroScreen( onAction: (IntroUiAction) -> Unit, ) { UnifestScaffold( - containerColor = Color.White, + containerColor = MaterialTheme.colorScheme.background, ) { innerPadding -> IntroContent( uiState = uiState, @@ -125,7 +87,7 @@ fun IntroScreen( LoadingWheel( modifier = Modifier .fillMaxSize() - .background(Color.White), + .background(MaterialTheme.colorScheme.background), ) } @@ -160,11 +122,29 @@ fun IntroContent( .verticalScroll(rememberScrollState()) .padding(bottom = 60.dp), ) { - InformationText() + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 64.dp, bottom = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(id = R.string.intro_info_title), + style = Title2, + color = MaterialTheme.colorScheme.onBackground, + ) + Text( + text = stringResource(id = R.string.intro_info_description), + style = BoothLocation, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } SearchTextField( searchText = uiState.searchText, updateSearchText = { text -> onAction(IntroUiAction.OnSearchTextUpdated(text)) }, - searchTextHintRes = R.string.intro_search_text_hint, + searchTextHintRes = designR.string.search_text_hint, onSearch = { onAction(IntroUiAction.OnSearch(it)) }, clearSearchText = { onAction(IntroUiAction.OnSearchTextCleared) }, modifier = Modifier @@ -179,7 +159,10 @@ fun IntroContent( ) if (uiState.selectedFestivals.isNotEmpty()) { Spacer(modifier = Modifier.height(21.dp)) - UnifestHorizontalDivider() + HorizontalDivider( + thickness = 8.dp, + color = MaterialTheme.colorScheme.outline, + ) } AllFestivalsTabRow( festivals = uiState.festivals, @@ -207,320 +190,15 @@ fun IntroContent( } } -@Composable -fun InformationText() { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 64.dp, bottom = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource(id = R.string.intro_info_title), - style = Title2, - color = Color.Black, - ) - Text( - text = stringResource(id = R.string.intro_info_description), - style = BoothLocation, - fontSize = 12.sp, - color = Color(0xFF848484), - ) - } -} - -@Composable -fun LikedFestivalsRow( - selectedFestivals: ImmutableList, - onAction: (IntroUiAction) -> Unit, -) { - Column { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - ) { - if (selectedFestivals.isNotEmpty()) { - Text( - text = stringResource(id = R.string.intro_liked_festivals_title), - style = Title3, - ) - TextButton( - onClick = { onAction(IntroUiAction.OnClearSelectionClick) }, - ) { - Text( - text = stringResource(id = R.string.intro_clear_item_button_text), - color = Color(0xFF848484), - textDecoration = TextDecoration.Underline, - style = Content6, - ) - } - } - } - LazyRow( - modifier = Modifier - .padding(8.dp) - .height(if (selectedFestivals.isEmpty()) 0.dp else 130.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - items( - count = selectedFestivals.size, - key = { index -> selectedFestivals[index].festivalId }, - ) { index -> - FestivalRowItem( - festival = selectedFestivals[index], - onAction = onAction, - ) - } - } - } -} - -@Composable -fun FestivalRowItem( - festival: FestivalModel, - onAction: (IntroUiAction) -> Unit, -) { - Card( - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = Color.White, contentColor = Color.Black), - border = BorderStroke(1.dp, MainColor), - modifier = Modifier - .height(130.dp) - .width(120.dp), - ) { - Box( - modifier = Modifier.clickable { - onAction(IntroUiAction.OnFestivalDeselected(festival)) - }, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - ) { - NetworkImage( - imgUrl = festival.thumbnail, - contentDescription = "Festival Thumbnail", - modifier = Modifier - .size(36.dp) - .clip(CircleShape), - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = festival.schoolName, - color = Color.Black, - style = Content2, - ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = festival.festivalName, - color = Color.Black, - style = Content4, - ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - "${festival.beginDate.toLocalDate().formatToString()} - ${festival.endDate.toLocalDate().formatToString()}", - color = Color(0xFF979797), - style = Content3, - ) - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun AllFestivalsTabRow( - festivals: ImmutableList, - isSearchLoading: Boolean, - onAction: (IntroUiAction) -> Unit, - selectedFestivals: ImmutableList, - modifier: Modifier = Modifier, -) { - val tabTitles = LocalContext.current.resources.getStringArray(R.array.region_tab_titles).toList() - val pagerState = rememberPagerState(pageCount = { tabTitles.size }) - val scope = rememberCoroutineScope() - val selectedTabIndex by remember { derivedStateOf { pagerState.currentPage } } - - Column(modifier) { - ScrollableTabRow( - selectedTabIndex = selectedTabIndex, - containerColor = Color.White, - contentColor = Color(0xFFE5E5E5), - edgePadding = 0.dp, - indicator = {}, - divider = { - HorizontalDivider( - color = Color(0xFFE5E5E5), - thickness = 1.75.dp, - ) - }, - ) { - tabTitles.forEachIndexed { index, title -> - val isSelected = selectedTabIndex == index - val tabTitleColor by animateColorAsState( - targetValue = if (isSelected) MainColor else Color.Black, - ) - val tabTitleFontWeight by animateFloatAsState( - targetValue = (if (isSelected) FontWeight.Bold.weight else FontWeight.Normal.weight).toFloat(), - ) - Tab( - selected = isSelected, - onClick = { - onAction(IntroUiAction.OnRegionTapClicked(title)) - scope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { - Text( - text = title, - color = tabTitleColor, - style = Content1, - fontWeight = FontWeight(weight = tabTitleFontWeight.toInt()), - ) - }, - ) - } - } - Text( - text = stringResource(id = R.string.total_festivals_count, festivals.size), - modifier = Modifier - .padding(start = 20.dp, top = 15.dp, bottom = 15.dp) - .align(Alignment.Start), - color = Color(0xFF4C4C4C), - style = Content6, - ) - HorizontalPager( - state = pagerState, - modifier = Modifier.fillMaxSize(), - ) { - Box( - modifier = Modifier.fillMaxSize(), - ) { - if (festivals.isEmpty()) { - Text( - text = stringResource(id = R.string.intro_no_result), - modifier = Modifier - .align(Alignment.Center) - .padding(bottom = 92.dp), - color = Color(0xFF7E7E7E), - style = Content3, - ) - } else { - LazyVerticalGrid( - columns = GridCells.Fixed(3), - modifier = Modifier - .padding(horizontal = 8.dp) - .height(if (festivals.isEmpty()) 0.dp else (((festivals.size - 1) / 3 + 1) * 140).dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(bottom = 48.dp), - ) { - items( - count = festivals.size, - key = { index -> festivals[index].festivalId }, - ) { index -> - FestivalItem( - festival = festivals[index], - onFestivalSelected = { festival -> - if (!selectedFestivals.any { it.festivalId == festival.festivalId }) { - onAction(IntroUiAction.OnFestivalSelected(festival)) - } - }, - ) - } - } - } - if (isSearchLoading) { - LoadingWheel( - modifier = Modifier - .fillMaxSize() - .align(Alignment.Center) - .background(Color.White) - .padding(bottom = 92.dp), - ) - } - } - } - } -} - @DevicePreview @Composable -fun IntroScreenPreview() { +fun IntroScreenPreview( + @PreviewParameter(IntroPreviewParameterProvider::class) + introUiState: IntroUiState, +) { UnifestTheme { IntroScreen( - uiState = IntroUiState( - festivals = persistentListOf( - FestivalModel( - 1, - 1, - "https://picsum.photos/36", - "서울대학교", - "서울", - "설대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 2, - 2, - "https://picsum.photos/36", - "연세대학교", - "서울", - "연대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 3, - 3, - "https://picsum.photos/36", - "고려대학교", - "서울", - "고대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 4, - 4, - "https://picsum.photos/36", - "성균관대학교", - "서울", - "성대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 5, - 5, - "https://picsum.photos/36", - "건국대학교", - "서울", - "건대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - ), - ), + uiState = introUiState, onAction = {}, ) } @@ -531,9 +209,7 @@ fun IntroScreenPreview() { fun IntroScreenEmptyPreview() { UnifestTheme { IntroScreen( - uiState = IntroUiState( - festivals = persistentListOf(), - ), + uiState = IntroUiState(festivals = persistentListOf()), onAction = {}, ) } diff --git a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/component/AllFestivalsRow.kt b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/component/AllFestivalsRow.kt new file mode 100644 index 00000000..20b9fdc2 --- /dev/null +++ b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/component/AllFestivalsRow.kt @@ -0,0 +1,227 @@ +package com.unifest.android.feature.intro.component + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.feature.intro.R +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.core.designsystem.component.LoadingWheel +import com.unifest.android.core.designsystem.theme.Content1 +import com.unifest.android.core.designsystem.theme.Content3 +import com.unifest.android.core.designsystem.theme.Content6 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.model.FestivalModel +import com.unifest.android.core.ui.component.FestivalItem +import com.unifest.android.feature.intro.viewmodel.IntroUiAction +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.launch + +@Composable +fun AllFestivalsTabRow( + festivals: ImmutableList, + isSearchLoading: Boolean, + onAction: (IntroUiAction) -> Unit, + selectedFestivals: ImmutableList, + modifier: Modifier = Modifier, +) { + val tabTitles = LocalContext.current.resources.getStringArray(R.array.region_tab_titles).toList() + val pagerState = rememberPagerState(pageCount = { tabTitles.size }) + val scope = rememberCoroutineScope() + val selectedTabIndex by remember { derivedStateOf { pagerState.currentPage } } + + Column(modifier) { + ScrollableTabRow( + selectedTabIndex = selectedTabIndex, + containerColor = MaterialTheme.colorScheme.background, + edgePadding = 0.dp, + indicator = {}, + divider = { + HorizontalDivider( + color = MaterialTheme.colorScheme.outline, + thickness = 1.75.dp, + ) + }, + ) { + tabTitles.forEachIndexed { index, title -> + val isSelected = selectedTabIndex == index + val tabTitleColor by animateColorAsState( + targetValue = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground, + ) + val tabTitleFontWeight by animateFloatAsState( + targetValue = (if (isSelected) FontWeight.Bold.weight else FontWeight.Normal.weight).toFloat(), + ) + Tab( + selected = isSelected, + onClick = { + if (!isSelected) { + onAction(IntroUiAction.OnRegionTapClicked(title)) + scope.launch { + pagerState.animateScrollToPage(index) + } + } + }, + text = { + Text( + text = title, + color = tabTitleColor, + style = Content1, + fontWeight = FontWeight(weight = tabTitleFontWeight.toInt()), + ) + }, + ) + } + } + Text( + text = stringResource(id = R.string.total_festivals_count, festivals.size), + modifier = Modifier + .padding(start = 20.dp, top = 15.dp, bottom = 15.dp) + .align(Alignment.Start), + color = MaterialTheme.colorScheme.onBackground, + style = Content6, + ) + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + ) { + Box( + modifier = Modifier.fillMaxSize(), + ) { + if (festivals.isEmpty()) { + Text( + text = stringResource(id = designR.string.no_result), + modifier = Modifier + .align(Alignment.Center) + .padding(bottom = 92.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = Content3, + ) + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(3), + modifier = Modifier + .padding(horizontal = 8.dp) + .height(if (festivals.isEmpty()) 0.dp else (((festivals.size - 1) / 3 + 1) * 140).dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(bottom = 48.dp), + ) { + items( + count = festivals.size, + key = { index -> festivals[index].festivalId }, + ) { index -> + FestivalItem( + festival = festivals[index], + onFestivalSelected = { festival -> + if (!selectedFestivals.any { it.festivalId == festival.festivalId }) { + onAction(IntroUiAction.OnFestivalSelected(festival)) + } + }, + ) + } + } + } + if (isSearchLoading) { + LoadingWheel( + modifier = Modifier + .fillMaxSize() + .align(Alignment.Center) + .background(MaterialTheme.colorScheme.background) + .padding(bottom = 92.dp), + ) + } + } + } + } +} + +@ComponentPreview +@Composable +private fun AllFestivalsTabRowPreview() { + UnifestTheme { + AllFestivalsTabRow( + festivals = persistentListOf( + FestivalModel( + 1, + 1, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 2, + 2, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 3, + 3, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + ), + isSearchLoading = false, + onAction = {}, + selectedFestivals = persistentListOf( + FestivalModel( + 1, + 1, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + ), + ) + } +} diff --git a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/component/FestivalRowItem.kt b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/component/FestivalRowItem.kt new file mode 100644 index 00000000..ad61c50b --- /dev/null +++ b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/component/FestivalRowItem.kt @@ -0,0 +1,111 @@ +package com.unifest.android.feature.intro.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.unifest.android.core.common.utils.formatToString +import com.unifest.android.core.common.utils.toLocalDate +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.component.NetworkImage +import com.unifest.android.core.designsystem.theme.Content2 +import com.unifest.android.core.designsystem.theme.Content3 +import com.unifest.android.core.designsystem.theme.Content4 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.model.FestivalModel +import com.unifest.android.feature.intro.viewmodel.IntroUiAction + +@Composable +fun FestivalRowItem( + festival: FestivalModel, + onAction: (IntroUiAction) -> Unit, +) { + Card( + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), + modifier = Modifier + .height(130.dp) + .width(120.dp), + ) { + Box( + modifier = Modifier.clickable { + onAction(IntroUiAction.OnFestivalDeselected(festival)) + }, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + ) { + NetworkImage( + imgUrl = festival.thumbnail, + contentDescription = "Festival Thumbnail", + modifier = Modifier + .size(36.dp) + .clip(CircleShape), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = festival.schoolName, + color = MaterialTheme.colorScheme.onBackground, + style = Content2, + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = festival.festivalName, + color = MaterialTheme.colorScheme.onBackground, + style = Content4, + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + "${festival.beginDate.toLocalDate().formatToString()} - ${festival.endDate.toLocalDate().formatToString()}", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = Content3, + ) + } + } + } +} + +@ComponentPreview +@Composable +private fun FestivalRowItemPreview() { + UnifestTheme { + FestivalRowItem( + festival = FestivalModel( + 1, + 1, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + onAction = {}, + ) + } +} diff --git a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/component/LikedFestivalsRow.kt b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/component/LikedFestivalsRow.kt new file mode 100644 index 00000000..74617b18 --- /dev/null +++ b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/component/LikedFestivalsRow.kt @@ -0,0 +1,98 @@ +package com.unifest.android.feature.intro.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.theme.Content6 +import com.unifest.android.core.designsystem.theme.Title3 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.model.FestivalModel +import com.unifest.android.feature.intro.R +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.feature.intro.viewmodel.IntroUiAction +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun LikedFestivalsRow( + selectedFestivals: ImmutableList, + onAction: (IntroUiAction) -> Unit, +) { + Column { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) { + if (selectedFestivals.isNotEmpty()) { + Text( + text = stringResource(id = designR.string.liked_festivals_title), + color = MaterialTheme.colorScheme.onBackground, + style = Title3, + ) + TextButton( + onClick = { onAction(IntroUiAction.OnClearSelectionClick) }, + ) { + Text( + text = stringResource(id = R.string.clear_item_button_text), + color = MaterialTheme.colorScheme.onSurfaceVariant, + textDecoration = TextDecoration.Underline, + style = Content6, + ) + } + } + } + LazyRow( + modifier = Modifier + .padding(8.dp) + .height(if (selectedFestivals.isEmpty()) 0.dp else 130.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items( + count = selectedFestivals.size, + key = { index -> selectedFestivals[index].festivalId }, + ) { index -> + FestivalRowItem( + festival = selectedFestivals[index], + onAction = onAction, + ) + } + } + } +} + +@ComponentPreview +@Composable +private fun LikedFestivalRowPreview() { + UnifestTheme { + LikedFestivalsRow( + selectedFestivals = persistentListOf( + FestivalModel( + festivalId = 0L, + schoolName = "건국대", + festivalName = "녹색지대", + beginDate = "2024-05-21", + endDate = "2024-05-23", + thumbnail = "", + ), + ), + onAction = {}, + ) + } +} diff --git a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/di/IntroNavigator.kt b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/di/IntroNavigator.kt index a2eaffbc..7fab2cba 100644 --- a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/di/IntroNavigator.kt +++ b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/di/IntroNavigator.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Intent import com.unifest.android.core.common.extension.startActivityWithAnimation import com.unifest.android.feature.intro.IntroActivity -import com.unifest.feature.navigator.IntroNavigator +import com.unifest.android.feature.navigator.IntroNavigator import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/preview/IntroPreviewParameterProvider.kt b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/preview/IntroPreviewParameterProvider.kt new file mode 100644 index 00000000..c81c5c43 --- /dev/null +++ b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/preview/IntroPreviewParameterProvider.kt @@ -0,0 +1,68 @@ +package com.unifest.android.feature.intro.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.unifest.android.core.model.FestivalModel +import com.unifest.android.feature.intro.viewmodel.IntroUiState +import kotlinx.collections.immutable.persistentListOf + +internal class IntroPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + IntroUiState( + festivals = persistentListOf( + FestivalModel( + festivalId = 1, + beginDate = "2024-04-05", + endDate = "2024-04-07", + festivalName = "녹색지대 DAY 1", + schoolName = "건국대학교 서울캠퍼스", + thumbnail = "https://picsum.photos/36", + schoolId = 1, + ), + FestivalModel( + festivalId = 2, + beginDate = "2024-04-05", + endDate = "2024-04-07", + festivalName = "녹색지대 DAY 1", + schoolName = "건국대학교 서울캠퍼스", + thumbnail = "https://picsum.photos/36", + schoolId = 2, + ), + FestivalModel( + festivalId = 3, + beginDate = "2024-04-05", + endDate = "2024-04-07", + festivalName = "녹색지대 DAY 1", + schoolName = "건국대학교 서울캠퍼스", + thumbnail = "https://picsum.photos/36", + schoolId = 3, + ), + ), + selectedFestivals = persistentListOf( + FestivalModel( + 1, + 1, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 2, + 2, + "https://picsum.photos/36", + "연세대학교", + "서울", + "연대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + ), + ), + ) +} diff --git a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/viewmodel/IntroViewModel.kt b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/viewmodel/IntroViewModel.kt index fc201e66..480a5608 100644 --- a/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/viewmodel/IntroViewModel.kt +++ b/feature/intro/src/main/kotlin/com/unifest/android/feature/intro/viewmodel/IntroViewModel.kt @@ -178,12 +178,14 @@ class IntroViewModel @Inject constructor( } } + // TODO 추후에 건국대로 고정하지 않고, 사용자가 선택한 학교로 변경 private fun addLikedFestivals() { viewModelScope.launch { _uiState.value.selectedFestivals.forEach { festival -> likedFestivalRepository.insertLikedFestivalAtSearch(festival) } - likedFestivalRepository.setRecentLikedFestival("건국대") + likedFestivalRepository.setRecentLikedFestival("한경대") + likedFestivalRepository.setRecentLikedFestivalId(2L) onboardingRepository.completeIntro(true) _uiEvent.send(IntroUiEvent.NavigateToMain) } diff --git a/feature/intro/src/main/res/values/strings.xml b/feature/intro/src/main/res/values/strings.xml new file mode 100644 index 00000000..ca4260ad --- /dev/null +++ b/feature/intro/src/main/res/values/strings.xml @@ -0,0 +1,20 @@ + + + Uni-Fest + 추가 완료 + 관심있는 학교 축제를 추가해보세요 + 관심 학교는 언제든지 수정 가능합니다 + + 전체 + 서울 + 경기/인천 + 강원 + 대전/충청 + 광주/전라 + 부산/대구 + 경상도 + + 총 %d개 + 모두 선택 해제 + + diff --git a/feature/liked-booth/build.gradle.kts b/feature/liked-booth/build.gradle.kts index 30c9fb7c..e4958e52 100644 --- a/feature/liked-booth/build.gradle.kts +++ b/feature/liked-booth/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.unifest.android.feature) + alias(libs.plugins.kotlin.serialization) // alias(libs.plugins.compose.investigator) } diff --git a/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/LikedBoothScreen.kt b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/LikedBoothScreen.kt index f738a4cf..8b09bb8e 100644 --- a/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/LikedBoothScreen.kt +++ b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/LikedBoothScreen.kt @@ -13,32 +13,31 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.unifest.android.core.common.ObserveAsEvents import com.unifest.android.core.common.UiText -import com.unifest.android.core.designsystem.R import com.unifest.android.core.designsystem.component.NetworkErrorDialog import com.unifest.android.core.designsystem.component.ServerErrorDialog import com.unifest.android.core.designsystem.component.TopAppBarNavigationType import com.unifest.android.core.designsystem.component.UnifestTopAppBar import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.android.core.model.LikedBoothModel import com.unifest.android.core.ui.DevicePreview import com.unifest.android.core.ui.component.EmptyLikedBoothItem import com.unifest.android.core.ui.component.LikedBoothItem +import com.unifest.android.feature.liked_booth.preview.LikedBoothPreviewParameterProvider import com.unifest.android.feature.liked_booth.viewmodel.ErrorType import com.unifest.android.feature.liked_booth.viewmodel.LikedBoothUiAction import com.unifest.android.feature.liked_booth.viewmodel.LikedBoothUiEvent import com.unifest.android.feature.liked_booth.viewmodel.LikedBoothUiState import com.unifest.android.feature.liked_booth.viewmodel.LikedBoothViewModel -import kotlinx.collections.immutable.persistentListOf @Composable internal fun LikedBoothRoute( @@ -75,6 +74,7 @@ internal fun LikedBoothScreen( Box( modifier = Modifier .fillMaxSize() + .background(MaterialTheme.colorScheme.background) .padding(padding), ) { Column { @@ -85,7 +85,7 @@ internal fun LikedBoothScreen( elevation = 8.dp, modifier = Modifier .background( - Color.White, + color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(bottomEnd = 20.dp, bottomStart = 20.dp), ) .padding(top = 13.dp, bottom = 5.dp), @@ -133,62 +133,14 @@ internal fun LikedBoothScreen( @DevicePreview @Composable -fun LikedBoothScreenPreview() { +fun LikedBoothScreenPreview( + @PreviewParameter(LikedBoothPreviewParameterProvider::class) + likedBoothUiState: LikedBoothUiState, +) { UnifestTheme { LikedBoothScreen( padding = PaddingValues(), - uiState = LikedBoothUiState( - likedBooths = persistentListOf( - LikedBoothModel( - id = 1, - name = "부스 이름", - category = "음식", - description = "부스 설명", - location = "부스 위치", - warning = "학과 전용 부스", - ), - LikedBoothModel( - id = 2, - name = "부스 이름", - category = "음식", - description = "부스 설명", - location = "부스 위치", - warning = "학과 전용 부스", - ), - LikedBoothModel( - id = 3, - name = "부스 이름", - category = "음식", - description = "부스 설명", - location = "부스 위치", - warning = "학과 전용 부스", - ), - LikedBoothModel( - id = 4, - name = "부스 이름", - category = "음식", - description = "부스 설명", - location = "부스 위치", - warning = "학과 전용 부스", - ), - LikedBoothModel( - id = 5, - name = "부스 이름", - category = "음식", - description = "부스 설명", - location = "부스 위치", - warning = "학과 전용 부스", - ), - LikedBoothModel( - id = 6, - name = "부스 이름", - category = "음식", - description = "부스 설명", - location = "부스 위치", - warning = "학과 전용 부스", - ), - ), - ), + uiState = likedBoothUiState, onAction = {}, ) } diff --git a/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/navigation/LikedBoothNavigation.kt b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/navigation/LikedBoothNavigation.kt index d4e37573..5bfcba25 100644 --- a/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/navigation/LikedBoothNavigation.kt +++ b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/navigation/LikedBoothNavigation.kt @@ -5,12 +5,11 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.unifest.android.core.common.UiText +import com.unifest.android.core.navigation.Route import com.unifest.android.feature.liked_booth.LikedBoothRoute -const val Liked_BOOTH_ROUTE = "liked_booth_route" - fun NavController.navigateToLikedBooth() { - navigate(Liked_BOOTH_ROUTE) + navigate(Route.LikeBooth) } fun NavGraphBuilder.likedBoothNavGraph( @@ -19,7 +18,7 @@ fun NavGraphBuilder.likedBoothNavGraph( navigateToBoothDetail: (Long) -> Unit, onShowSnackBar: (UiText) -> Unit, ) { - composable(route = Liked_BOOTH_ROUTE) { + composable { LikedBoothRoute( padding = padding, popBackStack = popBackStack, diff --git a/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/preview/LikedBoothPreviewParameterProvider.kt b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/preview/LikedBoothPreviewParameterProvider.kt new file mode 100644 index 00000000..0780eabf --- /dev/null +++ b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/preview/LikedBoothPreviewParameterProvider.kt @@ -0,0 +1,63 @@ +package com.unifest.android.feature.liked_booth.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.unifest.android.core.model.LikedBoothModel +import com.unifest.android.feature.liked_booth.viewmodel.LikedBoothUiState +import kotlinx.collections.immutable.persistentListOf + +internal class LikedBoothPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + LikedBoothUiState( + likedBooths = persistentListOf( + LikedBoothModel( + id = 1L, + name = "부스 이름", + category = "음식", + description = "부스 설명", + location = "부스 위치", + warning = "학과 전용 부스", + ), + LikedBoothModel( + id = 2L, + name = "부스 이름", + category = "음식", + description = "부스 설명", + location = "부스 위치", + warning = "학과 전용 부스", + ), + LikedBoothModel( + id = 3L, + name = "부스 이름", + category = "음식", + description = "부스 설명", + location = "부스 위치", + warning = "학과 전용 부스", + ), + LikedBoothModel( + id = 4L, + name = "부스 이름", + category = "음식", + description = "부스 설명", + location = "부스 위치", + warning = "학과 전용 부스", + ), + LikedBoothModel( + id = 5L, + name = "부스 이름", + category = "음식", + description = "부스 설명", + location = "부스 위치", + warning = "학과 전용 부스", + ), + LikedBoothModel( + id = 6L, + name = "부스 이름", + category = "음식", + description = "부스 설명", + location = "부스 위치", + warning = "학과 전용 부스", + ), + ), + ), + ) +} diff --git a/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothUiState.kt b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothUiState.kt index b33e033a..3bb49ee9 100644 --- a/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothUiState.kt +++ b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothUiState.kt @@ -7,7 +7,6 @@ import kotlinx.collections.immutable.persistentListOf data class LikedBoothUiState( val isLoading: Boolean = false, val likedBooths: ImmutableList = persistentListOf(), - // val likedBoothList: ImmutableList = persistentListOf(), val isServerErrorDialogVisible: Boolean = false, val isNetworkErrorDialogVisible: Boolean = false, ) diff --git a/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothViewModel.kt b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothViewModel.kt index b1a6d75e..166453dc 100644 --- a/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothViewModel.kt +++ b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothViewModel.kt @@ -7,8 +7,8 @@ import com.unifest.android.core.common.UiText import com.unifest.android.core.common.handleException import com.unifest.android.core.data.repository.BoothRepository import com.unifest.android.core.data.repository.LikedBoothRepository -import com.unifest.android.core.designsystem.R import com.unifest.android.core.model.LikedBoothModel +import com.unifest.android.core.designsystem.R as designR import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.channels.Channel @@ -34,7 +34,6 @@ class LikedBoothViewModel @Inject constructor( val uiEvent: Flow = _uiEvent.receiveAsFlow() init { - // observeLikedBooth() getLikedBooths() } @@ -60,18 +59,6 @@ class LikedBoothViewModel @Inject constructor( } } -// private fun observeLikedBooth() { -// viewModelScope.launch { -// likedBoothRepository.getLikedBoothList().collect { likedBoothList -> -// _uiState.update { -// it.copy( -// likedBooths = likedBoothList.toImmutableList(), -// ) -// } -// } -// } -// } - private fun navigateBack() { viewModelScope.launch { _uiEvent.send(LikedBoothUiEvent.NavigateBack) @@ -91,27 +78,13 @@ class LikedBoothViewModel @Inject constructor( updateLikedBooth(booth) delay(500) getLikedBooths() - _uiEvent.send(LikedBoothUiEvent.ShowSnackBar(UiText.StringResource(R.string.liked_booth_removed_message))) + _uiEvent.send(LikedBoothUiEvent.ShowSnackBar(UiText.StringResource(designR.string.liked_booth_removed_message))) }.onFailure { - _uiEvent.send(LikedBoothUiEvent.ShowSnackBar(UiText.StringResource(R.string.liked_booth_removed_failed_message))) + _uiEvent.send(LikedBoothUiEvent.ShowSnackBar(UiText.StringResource(designR.string.liked_booth_removed_failed_message))) } } } -// private fun deleteLikedBooth(booth: BoothDetailModel) { -// viewModelScope.launch { -// boothRepository.likeBooth(booth.id) -// .onSuccess { -// updateLikedBooth(booth) -// delay(500) -// likedBoothRepository.deleteLikedBooth(booth) -// _uiEvent.send(LikedBoothUiEvent.ShowSnackBar(UiText.StringResource(R.string.liked_booth_removed_message))) -// }.onFailure { -// _uiEvent.send(LikedBoothUiEvent.ShowSnackBar(UiText.StringResource(R.string.liked_booth_removed_failed_message))) -// } -// } -// } - private fun updateLikedBooth(booth: LikedBoothModel) { _uiState.update { it.copy( @@ -127,10 +100,6 @@ class LikedBoothViewModel @Inject constructor( } } -// private suspend fun updateLikedBooth(booth: BoothDetailModel) { -// likedBoothRepository.updateLikedBooth(booth.copy(isLiked = false)) -// } - override fun setServerErrorDialogVisible(flag: Boolean) { _uiState.update { it.copy(isServerErrorDialogVisible = flag) diff --git a/feature/liked-booth/src/main/res/values/strings.xml b/feature/liked-booth/src/main/res/values/strings.xml new file mode 100644 index 00000000..cee7bc7f --- /dev/null +++ b/feature/liked-booth/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + 관심 부스 + + diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index 5076468f..429c1e6b 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -2,17 +2,29 @@ plugins { alias(libs.plugins.unifest.android.feature) - alias(libs.plugins.unifest.android.retrofit) + alias(libs.plugins.kotlin.serialization) // alias(libs.plugins.compose.investigator) } android { namespace = "com.unifest.android.feature.main" + + android { + buildTypes { + debug { + isMinifyEnabled = false + } + release { + isMinifyEnabled = false + } + } + } } dependencies { implementations( projects.feature.booth, + projects.feature.festival, projects.feature.home, projects.feature.likedBooth, projects.feature.intro, @@ -20,12 +32,13 @@ dependencies { projects.feature.menu, projects.feature.navigator, projects.feature.waiting, + projects.feature.stamp, libs.androidx.activity.compose, libs.kotlinx.collections.immutable, libs.coil.compose, libs.compose.system.ui.controller, libs.androidx.navigation.compose, - + libs.timber, ) } diff --git a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainActivity.kt b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainActivity.kt index a5af30e3..1644a71c 100644 --- a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainActivity.kt +++ b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainActivity.kt @@ -1,15 +1,20 @@ package com.unifest.android.feature.main +import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.graphics.Color +import com.unifest.android.core.designsystem.theme.DarkGrey100 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.feature.navigator.IntroNavigator +import com.unifest.android.feature.navigator.IntroNavigator import dagger.hilt.android.AndroidEntryPoint import tech.thdev.compose.exteions.system.ui.controller.rememberExSystemUiController +import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint @@ -17,17 +22,20 @@ class MainActivity : ComponentActivity() { @Inject lateinit var introNavigator: IntroNavigator + private val viewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { val navigator: MainNavController = rememberMainNavController() val systemUiController = rememberExSystemUiController() + val isDarkTheme = isSystemInDarkTheme() DisposableEffect(systemUiController) { systemUiController.setSystemBarsColor( - color = Color.White, - darkIcons = true, + color = if (isDarkTheme) DarkGrey100 else Color.White, + darkIcons = !isDarkTheme, isNavigationBarContrastEnforced = false, ) @@ -37,8 +45,28 @@ class MainActivity : ComponentActivity() { UnifestTheme { MainScreen( navigator = navigator, + viewModel = viewModel, ) } } } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + + if (intent.getBooleanExtra("navigate_to_waiting", false)) { + val waitingId = intent.getStringExtra("waitingId") + Timber.d("navigate_to_waiting -> waitingId: $waitingId") + if (waitingId != null) { + viewModel.setWaitingId(waitingId.toLong()) + } + } else if (intent.getBooleanExtra("navigate_to_booth", false)) { + val boothId = intent.getStringExtra("boothId") + Timber.d("navigate_to_booth -> boothId: $boothId") + if (boothId != null) { + viewModel.setBoothId(boothId.toLong()) + } + } + } } diff --git a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainNavController.kt b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainNavController.kt index 9ca4e26c..1e3178f8 100644 --- a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainNavController.kt +++ b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainNavController.kt @@ -3,19 +3,22 @@ package com.unifest.android.feature.main import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.NavOptions import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions +import com.unifest.android.core.navigation.MainTabRoute +import com.unifest.android.core.navigation.Route import com.unifest.android.feature.booth.navigation.navigateToBoothDetail import com.unifest.android.feature.booth.navigation.navigateToBoothLocation import com.unifest.android.feature.home.navigation.navigateToHome import com.unifest.android.feature.liked_booth.navigation.navigateToLikedBooth -import com.unifest.android.feature.map.navigation.MAP_ROUTE import com.unifest.android.feature.map.navigation.navigateToMap import com.unifest.android.feature.menu.navigation.navigateToMenu +import com.unifest.android.feature.waiting.navigation.navigateToWaiting internal class MainNavController( val navController: NavHostController, @@ -24,12 +27,12 @@ internal class MainNavController( @Composable get() = navController .currentBackStackEntryAsState().value?.destination - val startDestination = MAP_ROUTE + val startDestination = MainTab.MAP.route val currentTab: MainTab? - @Composable get() = currentDestination - ?.route - ?.let(MainTab.Companion::find) + @Composable get() = MainTab.find { tab -> + currentDestination?.hasRoute(tab::class) == true + } fun navigate(tab: MainTab) { val navOptions = navOptions { @@ -42,8 +45,9 @@ internal class MainNavController( when (tab) { MainTab.HOME -> navController.navigateToHome(navOptions) + MainTab.WAITING -> navController.navigateToWaiting(navOptions) MainTab.MAP -> navController.navigateToMap(navOptions) -// MainTab.WAITING -> navController.navigateToWaiting(navOptions) + // MainTab.STAMP -> navController.navigateToStamp(navOptions) MainTab.MENU -> navController.navigateToMenu(navOptions) } } @@ -66,7 +70,7 @@ internal class MainNavController( // https://github.com/droidknights/DroidKnights2023_App/pull/243/commits/4bfb6d13908eaaab38ab3a59747d628efa3893cb fun popBackStackIfNotMap() { - if (!isSameCurrentDestination(MAP_ROUTE)) { + if (!isSameCurrentDestination()) { popBackStack() } } @@ -78,13 +82,13 @@ internal class MainNavController( navController.navigate(startDestination, options) } - private fun isSameCurrentDestination(route: String) = - navController.currentDestination?.route == route + private inline fun isSameCurrentDestination(): Boolean { + return navController.currentDestination?.hasRoute() == true + } @Composable - fun shouldShowBottomBar(): Boolean { - val currentRoute = currentDestination?.route ?: return false - return currentRoute in MainTab + fun shouldShowBottomBar() = MainTab.contains { + currentDestination?.hasRoute(it::class) == true } } diff --git a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainScreen.kt b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainScreen.kt index d0c2c732..4304dcff 100644 --- a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainScreen.kt +++ b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainScreen.kt @@ -1,58 +1,33 @@ package com.unifest.android.feature.main -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideIn -import androidx.compose.animation.slideOut -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.selection.selectable -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color.Companion.White -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import com.unifest.android.core.common.UiText -import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.component.UnifestScaffold import com.unifest.android.core.designsystem.component.UnifestSnackBar -import com.unifest.android.core.designsystem.theme.BottomMenuBar -import com.unifest.android.core.designsystem.theme.UnifestTheme import com.unifest.android.feature.booth.navigation.boothNavGraph import com.unifest.android.feature.home.navigation.homeNavGraph import com.unifest.android.feature.liked_booth.navigation.likedBoothNavGraph +import com.unifest.android.feature.main.component.MainBottomBar import com.unifest.android.feature.map.navigation.mapNavGraph import com.unifest.android.feature.menu.navigation.menuNavGraph +import com.unifest.android.feature.stamp.navigation.stampNavGraph import com.unifest.android.feature.waiting.navigation.waitingNavGraph -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -62,10 +37,13 @@ private const val SnackBarDuration = 1000L @Composable internal fun MainScreen( navigator: MainNavController = rememberMainNavController(), + viewModel: MainViewModel = hiltViewModel(), ) { val snackBarState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() val context = LocalContext.current + val waitingId by viewModel.waitingId.collectAsState() + val boothId by viewModel.boothId.collectAsState() val onShowSnackBar: (message: UiText) -> Unit = { message -> scope.launch { @@ -80,6 +58,20 @@ internal fun MainScreen( } } + LaunchedEffect(waitingId) { + if (waitingId != 0L) { + navigator.navigate(MainTab.WAITING) + viewModel.setWaitingId(0L) + } + } + + LaunchedEffect(boothId) { + if (boothId != 0L) { + navigator.navigateToBoothDetail(viewModel.boothId.value) + viewModel.setBoothId(0L) + } + } + UnifestScaffold( bottomBar = { MainBottomBar( @@ -126,6 +118,8 @@ internal fun MainScreen( ) waitingNavGraph( padding = innerPadding, + popBackStack = navigator::popBackStackIfNotMap, + navigateToBoothDetail = navigator::navigateToBoothDetail, ) menuNavGraph( padding = innerPadding, @@ -134,6 +128,11 @@ internal fun MainScreen( navigateToBoothDetail = navigator::navigateToBoothDetail, onShowSnackBar = onShowSnackBar, ) + stampNavGraph( + padding = innerPadding, + popBackStack = navigator::popBackStackIfNotMap, + navigateToBoothDetail = navigator::navigateToBoothDetail, + ) likedBoothNavGraph( padding = innerPadding, popBackStack = navigator::popBackStackIfNotMap, @@ -143,95 +142,3 @@ internal fun MainScreen( } } } - -@Composable -private fun MainBottomBar( - visible: Boolean, - tabs: ImmutableList, - currentTab: MainTab?, - onTabSelected: (MainTab) -> Unit, -) { - AnimatedVisibility( - visible = visible, - enter = fadeIn() + slideIn { IntOffset(0, it.height) }, - exit = fadeOut() + slideOut { IntOffset(0, it.height) }, - ) { - Box(modifier = Modifier.background(White)) { - Column { - HorizontalDivider(color = Color(0xFFEBEBEB)) - Row( - modifier = Modifier - .navigationBarsPadding() - .fillMaxWidth() - .height(64.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - tabs.forEach { tab -> - MainBottomBarItem( - tab = tab, - selected = tab == currentTab, - onClick = { - if (tab != currentTab) { - onTabSelected(tab) - } - }, - ) - } - } - } - } - } -} - -@Composable -private fun RowScope.MainBottomBarItem( - tab: MainTab, - selected: Boolean, - onClick: () -> Unit, -) { - Box( - modifier = Modifier - .weight(1f) - .fillMaxHeight() - .selectable( - selected = selected, - indication = null, - role = null, - interactionSource = remember { MutableInteractionSource() }, - onClick = onClick, - ), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - imageVector = if (selected) ImageVector.vectorResource(tab.selectedIconResId) - else ImageVector.vectorResource(tab.iconResId), - contentDescription = tab.contentDescription, - tint = Color.Unspecified, - ) - Spacer(modifier = Modifier.height(5.dp)) - Text( - text = tab.label, - color = if (selected) Color(0xFFFD067D) else Color(0xFF555555), - fontWeight = if (selected) FontWeight.SemiBold - else FontWeight.Normal, - style = BottomMenuBar, - ) - } - } -} - -@ComponentPreview -@Composable -fun MainBottomBarPreview() { - UnifestTheme { - MainBottomBar( - visible = true, - tabs = MainTab.entries.toImmutableList(), - currentTab = MainTab.HOME, - onTabSelected = {}, - ) - } -} diff --git a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainTab.kt b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainTab.kt index 4d02c59b..d772aeec 100644 --- a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainTab.kt +++ b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainTab.kt @@ -1,53 +1,63 @@ package com.unifest.android.feature.main -import com.unifest.android.core.designsystem.R +import androidx.compose.runtime.Composable +import com.unifest.android.core.navigation.MainTabRoute +import com.unifest.android.core.navigation.Route internal enum class MainTab( val iconResId: Int, val selectedIconResId: Int, - val contentDescription: String, + internal val contentDescription: String, val label: String, - val route: String, + val route: MainTabRoute, ) { HOME( iconResId = R.drawable.ic_home, selectedIconResId = R.drawable.ic_selected_home, contentDescription = "Home Icon", label = "홈", - route = "home_route", + route = MainTabRoute.Home, ), MAP( iconResId = R.drawable.ic_map, selectedIconResId = R.drawable.ic_selected_map, contentDescription = "Map Icon", label = "지도", - route = "map_route", + route = MainTabRoute.Map, + ), + WAITING( + iconResId = R.drawable.ic_waiting, + selectedIconResId = R.drawable.ic_selected_waiting, + contentDescription = "Waiting Icon", + label = "웨이팅", + route = MainTabRoute.Waiting, ), -// WAITING( -// iconResId = R.drawable.ic_waiting, -// selectedIconResId = R.drawable.ic_selected_waiting, -// contentDescription = "Waiting Icon", -// label = "웨이팅", -// route = "waiting_route", +// STAMP( +// iconResId = R.drawable.ic_stamp, +// selectedIconResId = R.drawable.ic_selected_stamp, +// contentDescription = "Stamp Icon", +// label = "스탬프", +// route = MainTabRoute.Stamp, // ), - MENU( iconResId = R.drawable.ic_menu, selectedIconResId = R.drawable.ic_selected_menu, contentDescription = "Menu Icon", label = "메뉴", - route = "menu_route", + route = MainTabRoute.Menu, ), ; companion object { - operator fun contains(route: String): Boolean { - return entries.map { it.route }.contains(route) + @Composable + fun find(predicate: @Composable (MainTabRoute) -> Boolean): MainTab? { + return entries.find { predicate(it.route) } } - fun find(route: String): MainTab? { - return entries.find { it.route == route } + @Composable + fun contains(predicate: @Composable (Route) -> Boolean): Boolean { + return entries.map { it.route }.any { predicate(it) } } } } diff --git a/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainViewModel.kt b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainViewModel.kt new file mode 100644 index 00000000..4bea0b4d --- /dev/null +++ b/feature/main/src/main/kotlin/com/unifest/android/feature/main/MainViewModel.kt @@ -0,0 +1,26 @@ +package com.unifest.android.feature.main + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor() : ViewModel() { + private val _waitingId: MutableStateFlow = MutableStateFlow(0L) + val waitingId: StateFlow = _waitingId.asStateFlow() + + private val _boothId: MutableStateFlow = MutableStateFlow(0L) + val boothId: StateFlow = _boothId.asStateFlow() + + fun setWaitingId(waitingId: Long) { + _waitingId.update { waitingId } + } + + fun setBoothId(boothId: Long) { + _boothId.update { boothId } + } +} diff --git a/feature/main/src/main/kotlin/com/unifest/android/feature/main/component/MainBottomBar.kt b/feature/main/src/main/kotlin/com/unifest/android/feature/main/component/MainBottomBar.kt new file mode 100644 index 00000000..64f71758 --- /dev/null +++ b/feature/main/src/main/kotlin/com/unifest/android/feature/main/component/MainBottomBar.kt @@ -0,0 +1,131 @@ +package com.unifest.android.feature.main.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideIn +import androidx.compose.animation.slideOut +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.theme.BottomMenuBar +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.feature.main.MainTab +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun MainBottomBar( + visible: Boolean, + tabs: ImmutableList, + currentTab: MainTab?, + onTabSelected: (MainTab) -> Unit, +) { + AnimatedVisibility( + visible = visible, + enter = fadeIn() + slideIn { IntOffset(0, it.height) }, + exit = fadeOut() + slideOut { IntOffset(0, it.height) }, + ) { + Box(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { + Column { + HorizontalDivider(color = MaterialTheme.colorScheme.outline) + Row( + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth() + .height(64.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + tabs.forEach { tab -> + MainBottomBarItem( + tab = tab, + selected = tab == currentTab, + onClick = { + if (tab != currentTab) { + onTabSelected(tab) + } + }, + ) + } + } + } + } + } +} + +@Composable +private fun RowScope.MainBottomBarItem( + tab: MainTab, + selected: Boolean, + onClick: () -> Unit, +) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .selectable( + selected = selected, + indication = null, + role = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = onClick, + ), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = if (selected) ImageVector.vectorResource(tab.selectedIconResId) + else ImageVector.vectorResource(tab.iconResId), + contentDescription = tab.contentDescription, + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = tab.label, + color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSecondary, + fontWeight = if (selected) FontWeight.SemiBold + else FontWeight.Normal, + style = BottomMenuBar, + ) + } + } +} + +@ComponentPreview +@Composable +private fun MainBottomBarPreview() { + UnifestTheme { + MainBottomBar( + visible = true, + tabs = MainTab.entries.toImmutableList(), + currentTab = MainTab.MAP, + onTabSelected = {}, + ) + } +} diff --git a/feature/main/src/main/kotlin/com/unifest/android/feature/main/di/MainNavigatorModule.kt b/feature/main/src/main/kotlin/com/unifest/android/feature/main/di/MainNavigatorModule.kt index dd6cd42d..d1df0f99 100644 --- a/feature/main/src/main/kotlin/com/unifest/android/feature/main/di/MainNavigatorModule.kt +++ b/feature/main/src/main/kotlin/com/unifest/android/feature/main/di/MainNavigatorModule.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Intent import com.unifest.android.core.common.extension.startActivityWithAnimation import com.unifest.android.feature.main.MainActivity -import com.unifest.feature.navigator.MainNavigator +import com.unifest.android.feature.navigator.MainNavigator import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/core/designsystem/src/main/res/drawable/ic_home.xml b/feature/main/src/main/res/drawable-night/ic_home.xml similarity index 96% rename from core/designsystem/src/main/res/drawable/ic_home.xml rename to feature/main/src/main/res/drawable-night/ic_home.xml index 7225837e..2e240902 100644 --- a/core/designsystem/src/main/res/drawable/ic_home.xml +++ b/feature/main/src/main/res/drawable-night/ic_home.xml @@ -7,5 +7,5 @@ android:pathData="M0.912,6.732L0.913,6.731C1.021,6.51 1.165,6.338 1.348,6.201L1.348,6.201L1.355,6.197L8.1,1.057C8.247,0.949 8.393,0.874 8.537,0.825C8.683,0.775 8.836,0.75 9,0.75C9.164,0.75 9.317,0.775 9.463,0.825C9.607,0.874 9.753,0.949 9.9,1.057L16.646,6.197L16.645,6.197L16.652,6.201C16.835,6.338 16.98,6.511 17.089,6.732C17.197,6.952 17.25,7.181 17.25,7.429V17.714C17.25,18.146 17.107,18.495 16.805,18.803C16.503,19.109 16.164,19.25 15.75,19.25H12V12V11.25H11.25H6.75H6V12V19.25H2.25C1.835,19.25 1.498,19.109 1.196,18.803C0.893,18.495 0.75,18.146 0.75,17.714V7.429C0.75,7.181 0.804,6.952 0.912,6.732ZM0.24,6.4C0.399,6.076 0.619,5.81 0.9,5.6L0.24,6.4Z" android:strokeWidth="1.5" android:fillColor="#00000000" - android:strokeColor="#555555"/> + android:strokeColor="#B6B8C1"/> diff --git a/core/designsystem/src/main/res/drawable/ic_map.xml b/feature/main/src/main/res/drawable-night/ic_map.xml similarity index 82% rename from core/designsystem/src/main/res/drawable/ic_map.xml rename to feature/main/src/main/res/drawable-night/ic_map.xml index 280045bb..b05b7bdd 100644 --- a/core/designsystem/src/main/res/drawable/ic_map.xml +++ b/feature/main/src/main/res/drawable-night/ic_map.xml @@ -7,10 +7,10 @@ android:pathData="M16.295,8.192C16.256,14.144 11.783,18.809 8.693,21.795C8.597,21.889 8.451,21.885 8.361,21.797C6.838,20.298 4.925,18.271 3.395,15.914C1.861,13.552 0.75,10.918 0.75,8.197C0.75,4.111 4.202,0.75 8.523,0.75C12.848,0.75 16.323,4.115 16.295,8.192Z" android:strokeWidth="1.5" android:fillColor="#00000000" - android:strokeColor="#555555"/> + android:strokeColor="#B6B8C1"/> + android:strokeColor="#B6B8C1"/> diff --git a/core/designsystem/src/main/res/drawable/ic_menu.xml b/feature/main/src/main/res/drawable-night/ic_menu.xml similarity index 85% rename from core/designsystem/src/main/res/drawable/ic_menu.xml rename to feature/main/src/main/res/drawable-night/ic_menu.xml index c7d0d6bf..b537467c 100644 --- a/core/designsystem/src/main/res/drawable/ic_menu.xml +++ b/feature/main/src/main/res/drawable-night/ic_menu.xml @@ -7,18 +7,18 @@ android:pathData="M1,1H19" android:strokeWidth="1.5" android:fillColor="#00000000" - android:strokeColor="#555555" + android:strokeColor="#B6B8C1" android:strokeLineCap="round"/> diff --git a/feature/main/src/main/res/drawable-night/ic_selected_home.xml b/feature/main/src/main/res/drawable-night/ic_selected_home.xml new file mode 100644 index 00000000..2e16c6f8 --- /dev/null +++ b/feature/main/src/main/res/drawable-night/ic_selected_home.xml @@ -0,0 +1,11 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_selected_map.xml b/feature/main/src/main/res/drawable-night/ic_selected_map.xml similarity index 73% rename from core/designsystem/src/main/res/drawable/ic_selected_map.xml rename to feature/main/src/main/res/drawable-night/ic_selected_map.xml index b9a519af..bb11f2c1 100644 --- a/core/designsystem/src/main/res/drawable/ic_selected_map.xml +++ b/feature/main/src/main/res/drawable-night/ic_selected_map.xml @@ -6,11 +6,11 @@ + android:fillColor="#3F0F1A" + android:strokeColor="#F55770"/> + android:fillColor="#212126" + android:strokeColor="#F55770"/> diff --git a/core/designsystem/src/main/res/drawable/ic_selected_menu.xml b/feature/main/src/main/res/drawable-night/ic_selected_menu.xml similarity index 85% rename from core/designsystem/src/main/res/drawable/ic_selected_menu.xml rename to feature/main/src/main/res/drawable-night/ic_selected_menu.xml index 02eca978..d60c6ccd 100644 --- a/core/designsystem/src/main/res/drawable/ic_selected_menu.xml +++ b/feature/main/src/main/res/drawable-night/ic_selected_menu.xml @@ -7,18 +7,18 @@ android:pathData="M1,1H19" android:strokeWidth="1.5" android:fillColor="#00000000" - android:strokeColor="#F5687E" + android:strokeColor="#F55770" android:strokeLineCap="round"/> diff --git a/core/designsystem/src/main/res/drawable/ic_selected_waiting.xml b/feature/main/src/main/res/drawable-night/ic_selected_waiting.xml similarity index 85% rename from core/designsystem/src/main/res/drawable/ic_selected_waiting.xml rename to feature/main/src/main/res/drawable-night/ic_selected_waiting.xml index 7504d73e..4d28b640 100644 --- a/core/designsystem/src/main/res/drawable/ic_selected_waiting.xml +++ b/feature/main/src/main/res/drawable-night/ic_selected_waiting.xml @@ -6,9 +6,9 @@ + android:fillColor="#3F0F1A" + android:strokeColor="#F55770"/> + android:fillColor="#F55770"/> diff --git a/core/designsystem/src/main/res/drawable/ic_waiting.xml b/feature/main/src/main/res/drawable-night/ic_waiting.xml similarity index 89% rename from core/designsystem/src/main/res/drawable/ic_waiting.xml rename to feature/main/src/main/res/drawable-night/ic_waiting.xml index c2f91b10..4fd55692 100644 --- a/core/designsystem/src/main/res/drawable/ic_waiting.xml +++ b/feature/main/src/main/res/drawable-night/ic_waiting.xml @@ -7,8 +7,8 @@ android:pathData="M10.5,10.5m-9.75,0a9.75,9.75 0,1 1,19.5 0a9.75,9.75 0,1 1,-19.5 0" android:strokeWidth="1.5" android:fillColor="#00000000" - android:strokeColor="#555555"/> + android:strokeColor="#B6B8C1"/> + android:fillColor="#B6B8C1"/> diff --git a/feature/main/src/main/res/drawable/ic_home.xml b/feature/main/src/main/res/drawable/ic_home.xml new file mode 100644 index 00000000..89f57750 --- /dev/null +++ b/feature/main/src/main/res/drawable/ic_home.xml @@ -0,0 +1,11 @@ + + + diff --git a/feature/main/src/main/res/drawable/ic_map.xml b/feature/main/src/main/res/drawable/ic_map.xml new file mode 100644 index 00000000..3d209b97 --- /dev/null +++ b/feature/main/src/main/res/drawable/ic_map.xml @@ -0,0 +1,16 @@ + + + + diff --git a/feature/main/src/main/res/drawable/ic_menu.xml b/feature/main/src/main/res/drawable/ic_menu.xml new file mode 100644 index 00000000..37253022 --- /dev/null +++ b/feature/main/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/feature/main/src/main/res/drawable/ic_selected_home.xml b/feature/main/src/main/res/drawable/ic_selected_home.xml new file mode 100644 index 00000000..9f908033 --- /dev/null +++ b/feature/main/src/main/res/drawable/ic_selected_home.xml @@ -0,0 +1,12 @@ + + + diff --git a/feature/main/src/main/res/drawable/ic_selected_map.xml b/feature/main/src/main/res/drawable/ic_selected_map.xml new file mode 100644 index 00000000..223f226b --- /dev/null +++ b/feature/main/src/main/res/drawable/ic_selected_map.xml @@ -0,0 +1,16 @@ + + + + diff --git a/feature/main/src/main/res/drawable/ic_selected_menu.xml b/feature/main/src/main/res/drawable/ic_selected_menu.xml new file mode 100644 index 00000000..7e971577 --- /dev/null +++ b/feature/main/src/main/res/drawable/ic_selected_menu.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/feature/main/src/main/res/drawable/ic_selected_stamp.xml b/feature/main/src/main/res/drawable/ic_selected_stamp.xml new file mode 100644 index 00000000..5ae3fa2a --- /dev/null +++ b/feature/main/src/main/res/drawable/ic_selected_stamp.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/feature/main/src/main/res/drawable/ic_selected_waiting.xml b/feature/main/src/main/res/drawable/ic_selected_waiting.xml new file mode 100644 index 00000000..2a3f2d8f --- /dev/null +++ b/feature/main/src/main/res/drawable/ic_selected_waiting.xml @@ -0,0 +1,14 @@ + + + + diff --git a/feature/main/src/main/res/drawable/ic_stamp.xml b/feature/main/src/main/res/drawable/ic_stamp.xml new file mode 100644 index 00000000..745b127c --- /dev/null +++ b/feature/main/src/main/res/drawable/ic_stamp.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/feature/main/src/main/res/drawable/ic_waiting.xml b/feature/main/src/main/res/drawable/ic_waiting.xml new file mode 100644 index 00000000..21d38673 --- /dev/null +++ b/feature/main/src/main/res/drawable/ic_waiting.xml @@ -0,0 +1,14 @@ + + + + diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index a51a5dbc..7f43a51a 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -2,8 +2,9 @@ plugins { alias(libs.plugins.unifest.android.feature) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.parcelize) // alias(libs.plugins.compose.investigator) - id("kotlin-parcelize") } android { diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/MapScreen.kt b/feature/map/src/main/kotlin/com/unifest/android/feature/map/MapScreen.kt index cab77acb..53b34bba 100644 --- a/feature/map/src/main/kotlin/com/unifest/android/feature/map/MapScreen.kt +++ b/feature/map/src/main/kotlin/com/unifest/android/feature/map/MapScreen.kt @@ -1,15 +1,21 @@ package com.unifest.android.feature.map import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.provider.Settings import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -19,24 +25,22 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -44,63 +48,76 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.naver.maps.geometry.LatLng import com.naver.maps.map.CameraPosition +import com.naver.maps.map.clustering.Clusterer +import com.naver.maps.map.clustering.DefaultClusterOnClickListener +import com.naver.maps.map.clustering.DefaultDistanceStrategy +import com.naver.maps.map.clustering.DefaultMarkerManager +import com.naver.maps.map.clustering.DistanceStrategy +import com.naver.maps.map.clustering.Node import com.naver.maps.map.compose.CameraPositionState +import com.naver.maps.map.compose.DisposableMapEffect import com.naver.maps.map.compose.ExperimentalNaverMapApi import com.naver.maps.map.compose.LocationTrackingMode import com.naver.maps.map.compose.MapProperties import com.naver.maps.map.compose.MapUiSettings -import com.naver.maps.map.compose.Marker -import com.naver.maps.map.compose.MarkerState import com.naver.maps.map.compose.NaverMap import com.naver.maps.map.compose.PolygonOverlay import com.naver.maps.map.compose.rememberCameraPositionState import com.naver.maps.map.compose.rememberFusedLocationSource +import com.naver.maps.map.overlay.Align +import com.naver.maps.map.overlay.Overlay +import com.naver.maps.map.overlay.OverlayImage import com.unifest.android.core.common.ObserveAsEvents +import com.unifest.android.core.common.PermissionDialogButtonType import com.unifest.android.core.common.UiText import com.unifest.android.core.common.extension.findActivity -import com.unifest.android.core.common.extension.goToAppSettings -import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.MarkerCategory -import com.unifest.android.core.designsystem.R import com.unifest.android.core.designsystem.component.NetworkErrorDialog -import com.unifest.android.core.designsystem.component.NetworkImage -import com.unifest.android.core.designsystem.component.SearchTextField import com.unifest.android.core.designsystem.component.ServerErrorDialog -import com.unifest.android.core.designsystem.component.TopAppBarNavigationType -import com.unifest.android.core.designsystem.component.UnifestTopAppBar -import com.unifest.android.core.designsystem.theme.Content2 -import com.unifest.android.core.designsystem.theme.MainColor -import com.unifest.android.core.designsystem.theme.Title2 import com.unifest.android.core.designsystem.theme.Title4 -import com.unifest.android.core.designsystem.theme.Title5 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.android.core.model.FestivalModel import com.unifest.android.core.ui.DevicePreview -import com.unifest.android.core.ui.component.BoothFilterChips +import com.unifest.android.core.ui.component.LocationPermissionTextProvider +import com.unifest.android.core.ui.component.NotificationPermissionTextProvider +import com.unifest.android.core.ui.component.PermissionDialog import com.unifest.android.feature.festival.FestivalSearchBottomSheet import com.unifest.android.feature.festival.viewmodel.FestivalUiAction import com.unifest.android.feature.festival.viewmodel.FestivalUiEvent import com.unifest.android.feature.festival.viewmodel.FestivalUiState import com.unifest.android.feature.festival.viewmodel.FestivalViewModel +import com.unifest.android.feature.map.component.BoothItem +import com.unifest.android.feature.map.component.MapTopAppBar import com.unifest.android.feature.map.model.BoothMapModel +import com.unifest.android.feature.map.model.ItemData +import com.unifest.android.feature.map.preview.MapPreviewParameterProvider import com.unifest.android.feature.map.viewmodel.ErrorType import com.unifest.android.feature.map.viewmodel.MapUiAction import com.unifest.android.feature.map.viewmodel.MapUiEvent import com.unifest.android.feature.map.viewmodel.MapUiState import com.unifest.android.feature.map.viewmodel.MapViewModel -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import com.unifest.android.core.designsystem.R as designR + +val permissionsToRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arrayOf( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.POST_NOTIFICATIONS, + ) +} else { + arrayOf( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ) +} @Composable internal fun MapRoute( @@ -114,17 +131,68 @@ internal fun MapRoute( val festivalUiState by festivalViewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current val activity = context.findActivity() + val dialogQueue = mapViewModel.permissionDialogQueue - val locationPermissions = arrayOf( - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION, - ) + var isLocationPermissionGranted by remember { + mutableStateOf(checkLocationPermission(activity)) + } + + var isNotificationPermissionGranted by remember { + mutableStateOf( + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + activity.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + } else { + true + }, + ) + } - val locationPermissionResultLauncher = rememberLauncherForActivityResult( + val permissionResultLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestMultiplePermissions(), - onResult = { permissions -> - val allPermissionsGranted = permissions.all { it.value } - mapViewModel.onPermissionResult(isGranted = allPermissionsGranted) + onResult = { perms -> + permissionsToRequest.forEach { permission -> + when (permission) { + Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION -> { + isLocationPermissionGranted = checkLocationPermission(activity) + } + + Manifest.permission.POST_NOTIFICATIONS -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + isNotificationPermissionGranted = perms[permission] == true + } + } + } + mapViewModel.onPermissionResult( + permission = permission, + isGranted = perms[permission] == true, + ) + } + }, + ) + + val settingsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { + // 설정에서 돌아왔을 때 권한 상태를 다시 확인 + isLocationPermissionGranted = checkLocationPermission(activity) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + isNotificationPermissionGranted = + activity.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + } + mapViewModel.onPermissionResult( + permission = Manifest.permission.ACCESS_FINE_LOCATION, + isGranted = isLocationPermissionGranted, + ) + mapViewModel.onPermissionResult( + permission = Manifest.permission.ACCESS_COARSE_LOCATION, + isGranted = isLocationPermissionGranted, + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mapViewModel.onPermissionResult( + permission = Manifest.permission.POST_NOTIFICATIONS, + isGranted = isNotificationPermissionGranted, + ) + } }, ) @@ -135,11 +203,21 @@ internal fun MapRoute( ObserveAsEvents(flow = mapViewModel.uiEvent) { event -> when (event) { - is MapUiEvent.RequestLocationPermission -> { - locationPermissionResultLauncher.launch(locationPermissions) + is MapUiEvent.RequestPermissions -> { + permissionResultLauncher.launch(permissionsToRequest) + } + + is MapUiEvent.RequestPermission -> { + permissionResultLauncher.launch(arrayOf(event.permission)) + } + + is MapUiEvent.NavigateToAppSetting -> { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", activity.packageName, null) + } + settingsLauncher.launch(intent) } - is MapUiEvent.GoToAppSettings -> activity.goToAppSettings() is MapUiEvent.NavigateToBoothDetail -> navigateToBoothDetail(event.boothId) is MapUiEvent.ShowSnackBar -> onShowSnackBar(event.message) } @@ -153,6 +231,60 @@ internal fun MapRoute( } } + dialogQueue + .reversed() + .forEach { permission -> + when (permission) { + Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION -> { + if (!isLocationPermissionGranted) { + PermissionDialog( + permissionTextProvider = LocationPermissionTextProvider(), + isPermanentlyDeclined = !activity.shouldShowRequestPermissionRationale(permission), + onDismiss = { + mapViewModel.onMapUiAction( + MapUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.DISMISS), + ) + }, + navigateToAppSetting = { + mapViewModel.onMapUiAction( + MapUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.NAVIGATE_TO_APP_SETTING), + ) + }, + onConfirm = { + mapViewModel.onMapUiAction( + MapUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.CONFIRM, permission), + ) + }, + ) + } + } + + Manifest.permission.POST_NOTIFICATIONS -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isNotificationPermissionGranted) { + PermissionDialog( + permissionTextProvider = NotificationPermissionTextProvider(), + isPermanentlyDeclined = !activity.shouldShowRequestPermissionRationale(permission), + onDismiss = { + mapViewModel.onMapUiAction( + MapUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.DISMISS), + ) + }, + navigateToAppSetting = { + mapViewModel.onMapUiAction( + MapUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.NAVIGATE_TO_APP_SETTING), + ) + }, + onConfirm = { + mapViewModel.onMapUiAction( + MapUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.CONFIRM, permission), + ) + }, + ) + } + } + } + } + MapScreen( padding = padding, mapUiState = mapUiState, @@ -162,7 +294,11 @@ internal fun MapRoute( ) } -@OptIn(ExperimentalFoundationApi::class) +private fun checkLocationPermission(activity: Activity): Boolean { + return activity.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || + activity.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED +} + @Composable internal fun MapScreen( padding: PaddingValues, @@ -171,9 +307,8 @@ internal fun MapScreen( onMapUiAction: (MapUiAction) -> Unit, onFestivalUiAction: (FestivalUiAction) -> Unit, ) { - val activity = LocalContext.current.findActivity() val cameraPositionState = rememberCameraPositionState { - position = CameraPosition(LatLng(37.5430, 127.07673671067072), 14.8) + position = CameraPosition(LatLng(37.0122749, 127.2635972), 15.8) } val rotationState by animateFloatAsState(targetValue = if (mapUiState.isPopularMode) 180f else 0f) val pagerState = rememberPagerState(pageCount = { mapUiState.selectedBoothList.size }) @@ -209,7 +344,7 @@ internal fun MapScreen( if (festivalUiState.isFestivalSearchBottomSheetVisible) { FestivalSearchBottomSheet( searchText = festivalUiState.festivalSearchText, - searchTextHintRes = R.string.festival_search_text_field_hint, + searchTextHintRes = designR.string.festival_search_text_field_hint, likedFestivals = festivalUiState.likedFestivals, festivalSearchResults = festivalUiState.festivalSearchResults, isSearchMode = festivalUiState.isSearchMode, @@ -218,17 +353,10 @@ internal fun MapScreen( isEditMode = festivalUiState.isEditMode, ) } - if (mapUiState.isPermissionDialogVisible) { - PermissionDialog( - permissionTextProvider = LocationPermissionTextProvider(), - isPermanentlyDeclined = !activity.shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION), - onMapUiAction = onMapUiAction, - ) - } } } -@OptIn(ExperimentalNaverMapApi::class, ExperimentalFoundationApi::class) +@OptIn(ExperimentalNaverMapApi::class) @Composable fun MapContent( uiState: MapUiState, @@ -240,19 +368,18 @@ fun MapContent( ) { // val context = LocalContext.current Box { - // TODO 같은 속성의 Marker 들만 클러스터링 되도록 구현 - // TODO 클러스터링 마커 커스텀 NaverMap( cameraPositionState = cameraPositionState, - locationSource = rememberFusedLocationSource(), + properties = MapProperties( + locationTrackingMode = LocationTrackingMode.NoFollow, + isNightModeEnabled = isSystemInDarkTheme(), + ), uiSettings = MapUiSettings( isZoomControlEnabled = false, isScaleBarEnabled = false, isLogoClickEnabled = false, ), - properties = MapProperties( - locationTrackingMode = LocationTrackingMode.NoFollow, - ), + locationSource = rememberFusedLocationSource(), ) { PolygonOverlay( coords = uiState.outerCords, @@ -261,47 +388,89 @@ fun MapContent( outlineWidth = 1.dp, holes = persistentListOf(uiState.innerHole), ) - uiState.filteredBoothsList.forEach { booth -> - Marker( - state = MarkerState(position = booth.position), - icon = MarkerCategory.fromString(booth.category).getMarkerIcon(booth.isSelected), - onClick = { - onMapUiAction(MapUiAction.OnBoothMarkerClick(booth)) - true - }, - ) - } -// var clusterManager by remember { mutableStateOf?>(null) } -// DisposableMapEffect(uiState.boothList) { map -> -// if (clusterManager == null) { -// clusterManager = Clusterer.Builder() -// .clusterMarkerUpdater(object : DefaultClusterMarkerUpdater() {}) -// .leafMarkerUpdater(object : DefaultLeafMarkerUpdater() { -// override fun updateLeafMarker(info: LeafMarkerInfo, marker: Marker) { -// super.updateLeafMarker(info, marker) -// marker.apply { -// icon = MarkerCategory.fromString((info.key as BoothMapModel).category).getMarkerIcon((info.key as BoothMapModel).isSelected) -// onClickListener = Overlay.OnClickListener { -// onMapUiAction(MapUiAction.OnBoothMarkerClick(listOf(info.key as BoothMapModel))) -// true -// } -// } -// } -// }) -// .build() -// .apply { this.map = map } -// } -// val boothListMap = buildMap(uiState.boothList.size) { -// uiState.boothList.forEachIndexed { index, booth -> -// put(booth, index) -// } -// } -// clusterManager?.addAll(boothListMap) -// onDispose { -// clusterManager?.clear() -// } +// uiState.filteredBoothList.forEach { booth -> +// Marker( +// state = MarkerState(position = booth.position), +// icon = MarkerCategory.fromString(booth.category).getMarkerIcon(booth.isSelected), +// onClick = { +// onMapUiAction(MapUiAction.OnBoothMarkerClick(booth)) +// true +// }, +// ) // } + var clusterManager by remember { mutableStateOf?>(null) } + DisposableMapEffect(uiState.filteredBoothList) { map -> + if (clusterManager == null) { + clusterManager = Clusterer.ComplexBuilder() + .minClusteringZoom(9) + .maxClusteringZoom(16) + .maxScreenDistance(200.0) + .thresholdStrategy { zoom -> + if (zoom <= 11) { + 0.0 + } else { + 70.0 + } + } + .distanceStrategy(object : DistanceStrategy { + private val defaultDistanceStrategy = DefaultDistanceStrategy() + + override fun getDistance(zoom: Int, node1: Node, node2: Node): Double { + return if (zoom <= 9) { + -1.0 + } else if ((node1.tag as ItemData).category == (node2.tag as ItemData).category) { + if (zoom <= 11) { + -1.0 + } else { + defaultDistanceStrategy.getDistance(zoom, node1, node2) + } + } else { + 10000.0 + } + } + }) + .tagMergeStrategy { cluster -> + if (cluster.maxZoom <= 9) { + null + } else { + ItemData("", (cluster.children.first().tag as ItemData).category) + } + } + .markerManager(object : DefaultMarkerManager() { + override fun createMarker() = super.createMarker().apply { + subCaptionTextSize = 10f + subCaptionColor = android.graphics.Color.WHITE + subCaptionHaloColor = android.graphics.Color.TRANSPARENT + } + }) + .clusterMarkerUpdater { info, marker -> + val size = info.size + marker.icon = OverlayImage.fromResource(designR.drawable.ic_cluster_icon) + marker.captionText = size.toString() + marker.setCaptionAligns(Align.Center) + marker.captionColor = android.graphics.Color.WHITE + marker.captionHaloColor = android.graphics.Color.TRANSPARENT + marker.onClickListener = DefaultClusterOnClickListener(info) + } + .leafMarkerUpdater { info, marker -> + marker.icon = MarkerCategory.fromString((info.key as BoothMapModel).category) + .getMarkerIcon((info.key as BoothMapModel).isSelected) + marker.onClickListener = Overlay.OnClickListener { + onMapUiAction(MapUiAction.OnBoothMarkerClick(listOf(info.key as BoothMapModel))) + true + } + } + .build() + .apply { this.map = map } + } + val boothListMap = uiState.filteredBoothList.associateWith { booth -> ItemData(booth.name, booth.category) } + clusterManager?.addAll(boothListMap) + onDispose { + clusterManager?.clear() + } + } + // var clusterManager by remember { mutableStateOf?>(null) } // DisposableMapEffect(uiState.filteredBoothsList) { map -> // if (clusterManager == null) { @@ -349,10 +518,10 @@ fun MapContent( .width(116.dp) .height(36.dp) .clip(RoundedCornerShape(39.dp)) - .background(Color.White) + .background(MaterialTheme.colorScheme.surface) .border( width = 1.dp, - color = MainColor, + color = MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(39.dp), ) .clickable { @@ -366,7 +535,7 @@ fun MapContent( ) { Text( text = stringResource(id = R.string.map_popular_booth), - color = MainColor, + color = MaterialTheme.colorScheme.primary, style = Title4, ) Spacer(modifier = Modifier.width(6.dp)) @@ -401,221 +570,19 @@ fun MapContent( } } -@Composable -fun MapTopAppBar( - title: String, - boothSearchText: TextFieldValue, - isOnboardingCompleted: Boolean, - onMapUiAction: (MapUiAction) -> Unit, - onFestivalUiAction: (FestivalUiAction) -> Unit, - selectedChips: ImmutableList, - modifier: Modifier = Modifier, -) { - Card( - modifier = modifier, - shape = RoundedCornerShape( - bottomStart = 32.dp, - bottomEnd = 32.dp, - ), - ) { - Column( - modifier = Modifier.background(Color.White), - ) { - UnifestTopAppBar( - navigationType = TopAppBarNavigationType.Search, - title = title, - onTitleClick = { onFestivalUiAction(FestivalUiAction.OnAddLikedFestivalClick) }, - isOnboardingCompleted = isOnboardingCompleted, - onTooltipClick = { onMapUiAction(MapUiAction.OnTooltipClick) }, - ) - SearchTextField( - searchText = boothSearchText, - updateSearchText = { text -> onMapUiAction(MapUiAction.OnSearchTextUpdated(text)) }, - searchTextHintRes = R.string.map_booth_search_text_field_hint, - onSearch = { onMapUiAction(MapUiAction.OnSearch(boothSearchText)) }, - clearSearchText = { onMapUiAction(MapUiAction.OnSearchTextCleared) }, - modifier = Modifier - .height(46.dp) - .fillMaxWidth() - .padding(horizontal = 20.dp), - ) - Spacer(modifier = Modifier.height(10.dp)) - BoothFilterChips( - onChipClick = { chip -> onMapUiAction(MapUiAction.OnBoothTypeChipClick(chip)) }, - selectedChips = selectedChips, - modifier = Modifier - .padding(horizontal = 10.dp) - .clip(RoundedCornerShape(16.dp)), - ) - Spacer(modifier = Modifier.height(10.dp)) - } - } -} - -@Composable -fun BoothItem( - boothInfo: BoothMapModel, - isPopularMode: Boolean, - ranking: Int, - onAction: (MapUiAction) -> Unit, - modifier: Modifier = Modifier, -) { - val density = LocalDensity.current - val textStyle = Content2 - val textHeight = remember(textStyle) { - with(density) { Content2.fontSize.toDp() * 2 } - } - - Card( - modifier = modifier.clickable { - onAction(MapUiAction.OnBoothItemClick(boothInfo.id)) - }, - colors = CardDefaults.cardColors(containerColor = Color.White), - shape = RoundedCornerShape(12.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - ) { - Box { - Row( - modifier = Modifier.padding(15.dp), - ) { - NetworkImage( - imgUrl = boothInfo.thumbnail, - contentDescription = "Booth Thumbnail", - modifier = Modifier - .size(86.dp) - .clip(RoundedCornerShape(16.dp)), - placeholder = painterResource(id = R.drawable.ic_item_placeholder), - ) - Column( - modifier = Modifier.padding(start = 15.dp), - ) { - Text( - text = boothInfo.name, - style = Title2, - ) - Spacer(modifier = Modifier.height(3.dp)) - Text( - text = boothInfo.description, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = Content2, - modifier = Modifier.heightIn(min = textHeight), - ) - Spacer(modifier = Modifier.height(3.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_location_green), - contentDescription = "Location Icon", - tint = Color.Unspecified, - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = boothInfo.location, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = Title5, - ) - } - } - } - if (isPopularMode) { - RankingBadge(ranking = ranking) - } - } - } -} - -@Composable -fun RankingBadge(ranking: Int) { - Box( - modifier = Modifier - .size(width = 43.dp, height = 45.dp) - .padding(start = 7.dp, top = 9.dp) - .clip(CircleShape) - .background(MainColor, CircleShape), - contentAlignment = Alignment.TopStart, - ) { - Text( - text = stringResource(id = R.string.map_ranking, ranking), - color = Color.White, - style = Title5, - modifier = Modifier.align(Alignment.Center), - ) - } -} - @DevicePreview @Composable -fun MapScreenPreview() { - val boothList = persistentListOf() - repeat(5) { index -> - boothList.add( - BoothMapModel( - id = index.toLong(), - name = "컴공 주점", - category = "", - description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", - location = "청심대 앞", - ), - ) - } - +private fun MapScreenPreview( + @PreviewParameter(MapPreviewParameterProvider::class) + mapUiState: MapUiState, +) { UnifestTheme { MapScreen( padding = PaddingValues(), - mapUiState = MapUiState( - festivalInfo = FestivalModel( - schoolName = "건국대학교", - ), - boothList = boothList, - ), + mapUiState = mapUiState, festivalUiState = FestivalUiState(), onMapUiAction = {}, onFestivalUiAction = {}, ) } } - -@ComponentPreview -@Composable -fun MapTopAppBarPreview() { - UnifestTheme { - MapTopAppBar( - title = "건국대학교", - boothSearchText = TextFieldValue(), - isOnboardingCompleted = false, - onMapUiAction = {}, - onFestivalUiAction = {}, - selectedChips = persistentListOf("주점", "먹거리"), - ) - } -} - -@ComponentPreview -@Composable -fun BoothItemPreview() { - UnifestTheme { - BoothItem( - boothInfo = BoothMapModel( - id = 1L, - name = "컴공 주점", - category = "", - description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", - location = "청심대 앞", - ), - isPopularMode = true, - ranking = 1, - onAction = {}, - ) - } -} - -@ComponentPreview -@Composable -fun RankingBadgePreview() { - UnifestTheme { - RankingBadge(ranking = 1) - } -} diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/component/BoothItem.kt b/feature/map/src/main/kotlin/com/unifest/android/feature/map/component/BoothItem.kt new file mode 100644 index 00000000..91739a74 --- /dev/null +++ b/feature/map/src/main/kotlin/com/unifest/android/feature/map/component/BoothItem.kt @@ -0,0 +1,167 @@ +package com.unifest.android.feature.map.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.core.designsystem.component.NetworkImage +import com.unifest.android.core.designsystem.theme.Content2 +import com.unifest.android.core.designsystem.theme.Title2 +import com.unifest.android.core.designsystem.theme.Title5 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.feature.map.R +import com.unifest.android.feature.map.model.BoothMapModel +import com.unifest.android.feature.map.viewmodel.MapUiAction + +@Composable +fun BoothItem( + boothInfo: BoothMapModel, + isPopularMode: Boolean, + ranking: Int, + onAction: (MapUiAction) -> Unit, + modifier: Modifier = Modifier, +) { + val density = LocalDensity.current + val textStyle = Content2 + val textHeight = remember(textStyle) { + with(density) { Content2.fontSize.toDp() * 2 } + } + + Card( + modifier = modifier.clickable { + onAction(MapUiAction.OnBoothItemClick(boothInfo.id)) + }, + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer), + shape = RoundedCornerShape(12.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + ) { + Box { + Row( + modifier = Modifier.padding(15.dp), + ) { + NetworkImage( + imgUrl = boothInfo.thumbnail, + contentDescription = "Booth Thumbnail", + modifier = Modifier + .size(86.dp) + .clip(RoundedCornerShape(16.dp)), + placeholder = painterResource(id = designR.drawable.item_placeholder), + ) + Column( + modifier = Modifier.padding(start = 15.dp), + ) { + Text( + text = boothInfo.name, + color = MaterialTheme.colorScheme.onBackground, + style = Title2, + ) + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = boothInfo.description, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.heightIn(min = textHeight), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = Content2, + ) + Spacer(modifier = Modifier.height(3.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_location_green), + contentDescription = "Location Icon", + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = boothInfo.location, + color = MaterialTheme.colorScheme.onSecondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = Title5, + ) + } + } + } + if (isPopularMode) { + RankingBadge(ranking = ranking) + } + } + } +} + +@Composable +fun RankingBadge(ranking: Int) { + Box( + modifier = Modifier + .size(width = 43.dp, height = 45.dp) + .padding(start = 7.dp, top = 9.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary, CircleShape), + contentAlignment = Alignment.TopStart, + ) { + Text( + text = stringResource(id = R.string.map_ranking, ranking), + color = MaterialTheme.colorScheme.onTertiaryContainer, + style = Title5, + modifier = Modifier.align(Alignment.Center), + ) + } +} + +@ComponentPreview +@Composable +fun BoothItemPreview() { + UnifestTheme { + BoothItem( + boothInfo = BoothMapModel( + id = 1L, + name = "컴공 주점", + category = "", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", + location = "청심대 앞", + ), + isPopularMode = true, + ranking = 1, + onAction = {}, + ) + } +} + +@ComponentPreview +@Composable +fun RankingBadgePreview() { + UnifestTheme { + RankingBadge(ranking = 1) + } +} diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/component/MapTopAppBar.kt b/feature/map/src/main/kotlin/com/unifest/android/feature/map/component/MapTopAppBar.kt new file mode 100644 index 00000000..70a636b8 --- /dev/null +++ b/feature/map/src/main/kotlin/com/unifest/android/feature/map/component/MapTopAppBar.kt @@ -0,0 +1,93 @@ +package com.unifest.android.feature.map.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.component.SearchTextField +import com.unifest.android.core.designsystem.component.TopAppBarNavigationType +import com.unifest.android.core.designsystem.component.UnifestTopAppBar +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.ui.component.BoothFilterChips +import com.unifest.android.feature.festival.viewmodel.FestivalUiAction +import com.unifest.android.feature.map.R +import com.unifest.android.feature.map.viewmodel.MapUiAction +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun MapTopAppBar( + title: String, + boothSearchText: TextFieldValue, + isOnboardingCompleted: Boolean, + onMapUiAction: (MapUiAction) -> Unit, + onFestivalUiAction: (FestivalUiAction) -> Unit, + selectedChips: ImmutableList, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier, + shape = RoundedCornerShape( + bottomStart = 32.dp, + bottomEnd = 32.dp, + ), + ) { + Column( + modifier = Modifier.background(MaterialTheme.colorScheme.background), + ) { + UnifestTopAppBar( + navigationType = TopAppBarNavigationType.Search, + title = title, + onTitleClick = { onFestivalUiAction(FestivalUiAction.OnAddLikedFestivalClick) }, + isOnboardingCompleted = isOnboardingCompleted, + onTooltipClick = { onMapUiAction(MapUiAction.OnTooltipClick) }, + ) + SearchTextField( + searchText = boothSearchText, + updateSearchText = { text -> onMapUiAction(MapUiAction.OnSearchTextUpdated(text)) }, + searchTextHintRes = R.string.map_booth_search_text_field_hint, + onSearch = { onMapUiAction(MapUiAction.OnSearch(boothSearchText)) }, + clearSearchText = { onMapUiAction(MapUiAction.OnSearchTextCleared) }, + modifier = Modifier + .height(46.dp) + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) + Spacer(modifier = Modifier.height(10.dp)) + BoothFilterChips( + onChipClick = { chip -> onMapUiAction(MapUiAction.OnBoothTypeChipClick(chip)) }, + selectedChips = selectedChips, + modifier = Modifier + .padding(horizontal = 10.dp) + .clip(RoundedCornerShape(16.dp)), + ) + Spacer(modifier = Modifier.height(10.dp)) + } + } +} + +@ComponentPreview +@Composable +fun MapTopAppBarPreview() { + UnifestTheme { + MapTopAppBar( + title = "건국대학교", + boothSearchText = TextFieldValue(), + isOnboardingCompleted = false, + onMapUiAction = {}, + onFestivalUiAction = {}, + selectedChips = persistentListOf("주점", "먹거리"), + ) + } +} diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/model/BoothMapModel.kt b/feature/map/src/main/kotlin/com/unifest/android/feature/map/model/BoothMapModel.kt index a1cde03d..61fd58a7 100644 --- a/feature/map/src/main/kotlin/com/unifest/android/feature/map/model/BoothMapModel.kt +++ b/feature/map/src/main/kotlin/com/unifest/android/feature/map/model/BoothMapModel.kt @@ -22,6 +22,8 @@ data class BoothMapModel( } } +data class ItemData(val name: String, val category: String) + // @Parcelize // data class BoothMapModel( // val id: Long = 0L, diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/navigation/MapNavigation.kt b/feature/map/src/main/kotlin/com/unifest/android/feature/map/navigation/MapNavigation.kt index dfce89de..1bc7e579 100644 --- a/feature/map/src/main/kotlin/com/unifest/android/feature/map/navigation/MapNavigation.kt +++ b/feature/map/src/main/kotlin/com/unifest/android/feature/map/navigation/MapNavigation.kt @@ -6,12 +6,11 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.unifest.android.core.common.UiText +import com.unifest.android.core.navigation.MainTabRoute import com.unifest.android.feature.map.MapRoute -const val MAP_ROUTE = "map_route" - fun NavController.navigateToMap(navOptions: NavOptions) { - navigate(MAP_ROUTE, navOptions) + navigate(MainTabRoute.Map, navOptions) } fun NavGraphBuilder.mapNavGraph( @@ -19,7 +18,7 @@ fun NavGraphBuilder.mapNavGraph( navigateToBoothDetail: (Long) -> Unit, onShowSnackBar: (UiText) -> Unit, ) { - composable(route = MAP_ROUTE) { + composable { MapRoute( padding = padding, navigateToBoothDetail = navigateToBoothDetail, diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/preview/MapPreviewParameterProvider.kt b/feature/map/src/main/kotlin/com/unifest/android/feature/map/preview/MapPreviewParameterProvider.kt new file mode 100644 index 00000000..2f30dd41 --- /dev/null +++ b/feature/map/src/main/kotlin/com/unifest/android/feature/map/preview/MapPreviewParameterProvider.kt @@ -0,0 +1,54 @@ +package com.unifest.android.feature.map.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.unifest.android.core.model.FestivalModel +import com.unifest.android.feature.map.model.BoothMapModel +import com.unifest.android.feature.map.viewmodel.MapUiState +import kotlinx.collections.immutable.persistentListOf + +internal class MapPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + MapUiState( + festivalInfo = FestivalModel( + schoolName = "건국대학교", + ), + boothList = persistentListOf( + BoothMapModel( + id = 0L, + name = "컴공 주점", + category = "", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", + location = "청심대 앞", + ), + BoothMapModel( + id = 1L, + name = "컴공 주점", + category = "", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", + location = "청심대 앞", + ), + BoothMapModel( + id = 2L, + name = "컴공 주점", + category = "", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", + location = "청심대 앞", + ), + BoothMapModel( + id = 3L, + name = "컴공 주점", + category = "", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", + location = "청심대 앞", + ), + BoothMapModel( + id = 4L, + name = "컴공 주점", + category = "", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다. 100번째 방문자에게 깜짝 선물 증정 이벤트를 하고 있으니 많은 관심 부탁드려요~!", + location = "청심대 앞", + ), + ), + ), + ) +} diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiAction.kt b/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiAction.kt index 467df880..4164e6e2 100644 --- a/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiAction.kt +++ b/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiAction.kt @@ -1,6 +1,7 @@ package com.unifest.android.feature.map.viewmodel import androidx.compose.ui.text.input.TextFieldValue +import com.unifest.android.core.common.PermissionDialogButtonType import com.unifest.android.feature.map.model.BoothMapModel sealed interface MapUiAction { @@ -8,13 +9,17 @@ sealed interface MapUiAction { data class OnSearchTextUpdated(val searchText: TextFieldValue) : MapUiAction data object OnSearchTextCleared : MapUiAction data class OnSearch(val searchText: TextFieldValue) : MapUiAction + data class OnBoothMarkerClick(val booths: List) : MapUiAction - // data class OnBoothMarkerClick(val booths: List) : MapUiAction - data class OnBoothMarkerClick(val booth: BoothMapModel) : MapUiAction + // data class OnBoothMarkerClick(val booth: BoothMapModel) : MapUiAction data object OnTogglePopularBooth : MapUiAction data class OnBoothItemClick(val boothId: Long) : MapUiAction data class OnRetryClick(val error: ErrorType) : MapUiAction - data class OnPermissionDialogButtonClick(val buttonType: PermissionDialogButtonType) : MapUiAction + data class OnPermissionDialogButtonClick( + val buttonType: PermissionDialogButtonType, + val permission: String? = null, + ) : MapUiAction + data class OnBoothTypeChipClick(val chipName: String) : MapUiAction } @@ -22,9 +27,3 @@ enum class ErrorType { NETWORK, SERVER, } - -enum class PermissionDialogButtonType { - DISMISS, - CONFIRM, - GO_TO_APP_SETTINGS, -} diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiEvent.kt b/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiEvent.kt index 8e7b7846..a751ba57 100644 --- a/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiEvent.kt +++ b/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiEvent.kt @@ -3,8 +3,9 @@ package com.unifest.android.feature.map.viewmodel import com.unifest.android.core.common.UiText sealed interface MapUiEvent { - data object RequestLocationPermission : MapUiEvent - data object GoToAppSettings : MapUiEvent + data object RequestPermissions : MapUiEvent + data class RequestPermission(val permission: String) : MapUiEvent + data object NavigateToAppSetting : MapUiEvent data class NavigateToBoothDetail(val boothId: Long) : MapUiEvent data class ShowSnackBar(val message: UiText) : MapUiEvent } diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiState.kt b/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiState.kt index c39d3ca4..90109daf 100644 --- a/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiState.kt +++ b/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapUiState.kt @@ -17,7 +17,7 @@ data class MapUiState( val boothSearchText: TextFieldValue = TextFieldValue(), val festivalSearchResults: ImmutableList = persistentListOf(), val selectedBoothTypeChips: ImmutableList = persistentListOf("주점", "먹거리", "이벤트", "일반"), - val filteredBoothsList: ImmutableList = persistentListOf(), + val filteredBoothList: ImmutableList = persistentListOf(), val isPopularMode: Boolean = false, val isBoothSelectionMode: Boolean = false, val isMapOnboardingCompleted: Boolean = false, @@ -31,42 +31,39 @@ data class MapUiState( LatLng(30.0, 150.0), ), val innerHole: ImmutableList = persistentListOf( - LatLng(37.54470, 127.07615), - LatLng(37.54461, 127.07561), - LatLng(37.54478, 127.07553), - LatLng(37.54462, 127.07507), - LatLng(37.54461, 127.07455), - LatLng(37.54470, 127.07396), - LatLng(37.54462, 127.07348), - LatLng(37.54473, 127.07293), - LatLng(37.54195, 127.07162), - LatLng(37.54183, 127.07218), - LatLng(37.54100, 127.07311), - LatLng(37.53950, 127.07238), - LatLng(37.53873, 127.07504), - LatLng(37.53933, 127.07516), - LatLng(37.53919, 127.07674), - LatLng(37.53908, 127.07719), - LatLng(37.53910, 127.07839), - LatLng(37.53900, 127.07850), - LatLng(37.53902, 127.07903), - LatLng(37.53886, 127.07906), - LatLng(37.53891, 127.07995), - LatLng(37.53925, 127.07993), - LatLng(37.53934, 127.07973), - LatLng(37.53982, 127.07962), - LatLng(37.54014, 127.07999), - LatLng(37.54067, 127.08086), - LatLng(37.54119, 127.08131), - LatLng(37.54208, 127.08131), - LatLng(37.54234, 127.08115), - LatLng(37.54257, 127.07857), - LatLng(37.54382, 127.07876), - LatLng(37.54394, 127.07966), - LatLng(37.54429, 127.08006), - LatLng(37.54496, 127.07994), - LatLng(37.54493, 127.07897), - LatLng(37.54485, 127.07754), - LatLng(37.54494, 127.07704), + LatLng(37.0125281, 127.2598307), + LatLng(37.0101251, 127.2631513), + LatLng(37.0095553, 127.2639023), + LatLng(37.0094097, 127.2642242), + LatLng(37.0096453, 127.264546), + LatLng(37.0098295, 127.2649537), + LatLng(37.0100737, 127.2654687), + LatLng(37.0102578, 127.2656458), + LatLng(37.010502, 127.2659032), + LatLng(37.0112431, 127.2661661), + LatLng(37.0113802, 127.2661862), + LatLng(37.0114947, 127.2661782), + LatLng(37.0122818, 127.2659301), + LatLng(37.0125174, 127.2658979), + LatLng(37.0127305, 127.2659019), + LatLng(37.0130015, 127.2659703), + LatLng(37.0130464, 127.2657544), + LatLng(37.0131846, 127.2654915), + LatLng(37.0132178, 127.2654245), + LatLng(37.0133816, 127.2649189), + LatLng(37.0135626, 127.2649269), + LatLng(37.0136301, 127.2645246), + LatLng(37.013629, 127.2643958), + LatLng(37.0134652, 127.2634343), + LatLng(37.0141269, 127.2632143), + LatLng(37.0140359, 127.262694), + LatLng(37.0139781, 127.2626028), + LatLng(37.013266, 127.2617921), + LatLng(37.0133602, 127.2616184), + LatLng(37.0133709, 127.2615789), + LatLng(37.0128826, 127.2611242), + LatLng(37.0128044, 127.2610478), + LatLng(37.0131423, 127.2604087), + LatLng(37.0125265, 127.2598301), ), ) diff --git a/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapViewModel.kt b/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapViewModel.kt index 0fd49750..07106d33 100644 --- a/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapViewModel.kt +++ b/feature/map/src/main/kotlin/com/unifest/android/feature/map/viewmodel/MapViewModel.kt @@ -1,16 +1,19 @@ package com.unifest.android.feature.map.viewmodel +import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.unifest.android.core.common.ErrorHandlerActions +import com.unifest.android.core.common.PermissionDialogButtonType import com.unifest.android.core.common.UiText import com.unifest.android.core.common.handleException import com.unifest.android.core.data.repository.BoothRepository import com.unifest.android.core.data.repository.FestivalRepository import com.unifest.android.core.data.repository.LikedFestivalRepository import com.unifest.android.core.data.repository.OnboardingRepository -import com.unifest.android.core.designsystem.R +import com.unifest.android.core.model.FestivalModel +import com.unifest.android.feature.map.R import com.unifest.android.feature.map.mapper.toMapModel import com.unifest.android.feature.map.model.BoothMapModel import dagger.hilt.android.lifecycle.HiltViewModel @@ -24,6 +27,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -39,6 +43,21 @@ class MapViewModel @Inject constructor( private val _uiEvent = Channel() val uiEvent: Flow = _uiEvent.receiveAsFlow() + val permissionDialogQueue = mutableStateListOf() + + fun onPermissionResult( + permission: String, + isGranted: Boolean, + ) { +// if (isGranted && permissionDialogQueue.isEmpty()) { +// refreshFCMToken() +// } + + if (!isGranted && !permissionDialogQueue.contains(permission)) { + permissionDialogQueue.add(permission) + } + } + init { requestLocationPermission() searchSchoolName() @@ -48,13 +67,7 @@ class MapViewModel @Inject constructor( private fun requestLocationPermission() { viewModelScope.launch { - _uiEvent.send(MapUiEvent.RequestLocationPermission) - } - } - - fun onPermissionResult(isGranted: Boolean) { - if (!isGranted) { - _uiState.update { it.copy(isPermissionDialogVisible = true) } + _uiEvent.send(MapUiEvent.RequestPermissions) } } @@ -64,13 +77,13 @@ class MapViewModel @Inject constructor( is MapUiAction.OnSearchTextCleared -> clearBoothSearchText() is MapUiAction.OnSearch -> searchBooth() is MapUiAction.OnTooltipClick -> completeMapOnboarding() - // is MapUiAction.OnBoothMarkerClick -> updateSelectedBoothList(action.booths) - is MapUiAction.OnBoothMarkerClick -> updateSelectedBooth(action.booth) + is MapUiAction.OnBoothMarkerClick -> updateSelectedBoothList(action.booths) + // is MapUiAction.OnBoothMarkerClick -> updateSelectedBooth(action.booth) is MapUiAction.OnTogglePopularBooth -> setEnablePopularMode() is MapUiAction.OnBoothItemClick -> navigateToBoothDetail(action.boothId) is MapUiAction.OnRetryClick -> refresh(action.error) is MapUiAction.OnBoothTypeChipClick -> updateSelectedBoothChipList(action.chipName) - is MapUiAction.OnPermissionDialogButtonClick -> handlePermissionDialogButtonClick(action.buttonType) + is MapUiAction.OnPermissionDialogButtonClick -> handlePermissionDialogButtonClick(action.buttonType, action.permission) } } @@ -88,38 +101,38 @@ class MapViewModel @Inject constructor( filterBoothsByType(_uiState.value.selectedBoothTypeChips) } - private fun handlePermissionDialogButtonClick(buttonType: PermissionDialogButtonType) { + private fun handlePermissionDialogButtonClick(buttonType: PermissionDialogButtonType, permission: String?) { when (buttonType) { - PermissionDialogButtonType.CONFIRM -> { - viewModelScope.launch { - _uiState.update { - it.copy(isPermissionDialogVisible = false) - } - _uiEvent.send(MapUiEvent.RequestLocationPermission) - } + PermissionDialogButtonType.DISMISS -> { + dismissDialog() } - PermissionDialogButtonType.GO_TO_APP_SETTINGS -> { + PermissionDialogButtonType.NAVIGATE_TO_APP_SETTING -> { viewModelScope.launch { - _uiEvent.send(MapUiEvent.GoToAppSettings) + _uiEvent.send(MapUiEvent.NavigateToAppSetting) } } - PermissionDialogButtonType.DISMISS -> { - _uiState.update { - it.copy(isPermissionDialogVisible = false) + PermissionDialogButtonType.CONFIRM -> { + dismissDialog() + viewModelScope.launch { + _uiEvent.send(MapUiEvent.RequestPermission(permission!!)) } } } } + private fun dismissDialog() { + permissionDialogQueue.removeFirst() + } + private fun filterBoothsByType(chipList: List) { val englishCategories = chipList.map { it.toEnglishCategory() } val filteredBooths = _uiState.value.boothList.filter { booth -> englishCategories.contains(booth.category) } _uiState.update { - it.copy(filteredBoothsList = filteredBooths.toImmutableList()) + it.copy(filteredBoothList = filteredBooths.toImmutableList()) } } @@ -145,6 +158,7 @@ class MapViewModel @Inject constructor( _uiState.update { it.copy(festivalInfo = festivals[0]) } + addLikeFestival(festivals[0]) } } .onFailure { exception -> @@ -152,10 +166,21 @@ class MapViewModel @Inject constructor( } } } + private fun addLikeFestival(festival: FestivalModel) { + viewModelScope.launch { + likedFestivalRepository.registerLikedFestival() + .onSuccess { + likedFestivalRepository.insertLikedFestivalAtSearch(festival) + } + .onFailure { exception -> + Timber.e(exception) + } + } + } fun getPopularBooths() { viewModelScope.launch { - boothRepository.getPopularBooths(1) + boothRepository.getPopularBooths(2) .onSuccess { booths -> _uiState.update { it.copy(popularBoothList = booths.toImmutableList()) @@ -175,7 +200,7 @@ class MapViewModel @Inject constructor( fun getAllBooths() { viewModelScope.launch { - boothRepository.getAllBooths(1) + boothRepository.getAllBooths(2) .onSuccess { booths -> _uiState.update { it.copy( @@ -257,13 +282,13 @@ class MapViewModel @Inject constructor( private fun setEnablePopularMode() { if (_uiState.value.isBoothSelectionMode) { viewModelScope.launch { - boothRepository.getPopularBooths(1) + boothRepository.getPopularBooths(festivalId = 2) .onSuccess { booths -> _uiState.update { currentState -> currentState.copy( selectedBoothList = booths.map { it.toMapModel() }.toImmutableList(), isBoothSelectionMode = false, - filteredBoothsList = currentState.filteredBoothsList.map { booth -> + filteredBoothList = currentState.filteredBoothList.map { booth -> booth.copy(isSelected = false) }.toImmutableList(), ) @@ -289,42 +314,44 @@ class MapViewModel @Inject constructor( } } - private fun updateSelectedBooth(booth: BoothMapModel) { - if (_uiState.value.isPopularMode) { - viewModelScope.launch { - _uiState.update { - it.copy(isPopularMode = false) - } - delay(500) - _uiState.update { - it.copy( - isBoothSelectionMode = true, - selectedBoothList = listOf(booth).toImmutableList(), - ) - } - } - } else { - _uiState.update { - it.copy( - isBoothSelectionMode = true, - selectedBoothList = listOf(booth).toImmutableList(), - ) - } - } - _uiState.update { - it.copy( - filteredBoothsList = it.filteredBoothsList.map { boothMapModel -> - if (boothMapModel.id == booth.id) { - boothMapModel.copy(isSelected = true) - } else { - boothMapModel.copy(isSelected = false) - } - }.toImmutableList(), - ) - } - } +// private fun updateSelectedBooth(booth: BoothMapModel) { +// if (_uiState.value.isPopularMode) { +// viewModelScope.launch { +// _uiState.update { +// it.copy(isPopularMode = false) +// } +// delay(500) +// _uiState.update { +// it.copy( +// isBoothSelectionMode = true, +// selectedBoothList = listOf(booth).toImmutableList(), +// ) +// } +// } +// } else { +// _uiState.update { +// it.copy( +// isBoothSelectionMode = true, +// selectedBoothList = listOf(booth).toImmutableList(), +// ) +// } +// } +// _uiState.update { +// it.copy( +// filteredBoothList = it.filteredBoothList.map { boothMapModel -> +// if (boothMapModel.id == booth.id) { +// boothMapModel.copy(isSelected = true) +// } else { +// boothMapModel.copy(isSelected = false) +// } +// }.toImmutableList(), +// ) +// } +// } private fun updateSelectedBoothList(booths: List) { + Timber.d("booths.size: ${booths.size} updateSelectedBoothList: $booths") + Timber.d("filteredBoothsList: ${_uiState.value.filteredBoothList}") if (_uiState.value.isPopularMode) { viewModelScope.launch { _uiState.update { @@ -349,7 +376,7 @@ class MapViewModel @Inject constructor( if (booths.size == 1) { _uiState.update { it.copy( - filteredBoothsList = it.filteredBoothsList.map { boothMapModel -> + filteredBoothList = it.filteredBoothList.map { boothMapModel -> if (boothMapModel.id == booths[0].id) { boothMapModel.copy(isSelected = true) } else { @@ -358,6 +385,7 @@ class MapViewModel @Inject constructor( }.toImmutableList(), ) } + Timber.d("booths.size: ${booths.size} updateSelectedBoothList: $booths") } } diff --git a/core/designsystem/src/main/res/drawable/ic_dropdown.xml b/feature/map/src/main/res/drawable/ic_dropdown.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_dropdown.xml rename to feature/map/src/main/res/drawable/ic_dropdown.xml diff --git a/feature/map/src/main/res/values/strings.xml b/feature/map/src/main/res/values/strings.xml new file mode 100644 index 00000000..8846be44 --- /dev/null +++ b/feature/map/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + 부스 / 주점을 검색해보세요. + 인기 부스 + %d위 + 인기 부스 갱신을 실패했습니다. + 검색 결과가 없습니다 + + diff --git a/feature/menu/build.gradle.kts b/feature/menu/build.gradle.kts index cecdda7f..15ff5639 100644 --- a/feature/menu/build.gradle.kts +++ b/feature/menu/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.unifest.android.feature) + alias(libs.plugins.kotlin.serialization) // alias(libs.plugins.compose.investigator) } diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/MenuScreen.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/MenuScreen.kt index 40ac4b13..a44c94b9 100644 --- a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/MenuScreen.kt +++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/MenuScreen.kt @@ -4,7 +4,6 @@ import android.content.pm.PackageManager import android.widget.Toast import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -17,16 +16,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -35,38 +31,27 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler -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.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.unifest.android.core.common.ObserveAsEvents import com.unifest.android.core.common.UiText -import com.unifest.android.core.designsystem.R import com.unifest.android.core.designsystem.component.NetworkErrorDialog -import com.unifest.android.core.designsystem.component.NetworkImage import com.unifest.android.core.designsystem.component.ServerErrorDialog import com.unifest.android.core.designsystem.component.TopAppBarNavigationType -import com.unifest.android.core.designsystem.component.UnifestHorizontalDivider import com.unifest.android.core.designsystem.component.UnifestTopAppBar -import com.unifest.android.core.designsystem.theme.Content6 import com.unifest.android.core.designsystem.theme.Content7 -import com.unifest.android.core.designsystem.theme.Content8 -import com.unifest.android.core.designsystem.theme.MenuTitle import com.unifest.android.core.designsystem.theme.Title3 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.android.core.model.FestivalModel -import com.unifest.android.core.model.LikedBoothModel import com.unifest.android.core.ui.DevicePreview import com.unifest.android.core.ui.component.EmptyLikedBoothItem import com.unifest.android.core.ui.component.LikedBoothItem @@ -75,13 +60,16 @@ import com.unifest.android.feature.festival.viewmodel.FestivalUiAction import com.unifest.android.feature.festival.viewmodel.FestivalUiEvent import com.unifest.android.feature.festival.viewmodel.FestivalUiState import com.unifest.android.feature.festival.viewmodel.FestivalViewModel +import com.unifest.android.feature.menu.component.FestivalItem +import com.unifest.android.feature.menu.component.MenuItem +import com.unifest.android.feature.menu.preview.MenuPreviewParameterProvider import com.unifest.android.feature.menu.viewmodel.ErrorType import com.unifest.android.feature.menu.viewmodel.MenuUiAction import com.unifest.android.feature.menu.viewmodel.MenuUiEvent import com.unifest.android.feature.menu.viewmodel.MenuUiState import com.unifest.android.feature.menu.viewmodel.MenuViewModel -import kotlinx.collections.immutable.persistentListOf import timber.log.Timber +import com.unifest.android.core.designsystem.R as designR @Composable internal fun MenuRoute( @@ -140,7 +128,6 @@ internal fun MenuRoute( ) } -@OptIn(ExperimentalFoundationApi::class) @Composable fun MenuScreen( padding: PaddingValues, @@ -153,6 +140,7 @@ fun MenuScreen( Box( modifier = Modifier .fillMaxSize() + .background(MaterialTheme.colorScheme.background) .padding(padding), ) { Column { @@ -162,7 +150,7 @@ fun MenuScreen( elevation = 8.dp, modifier = Modifier .background( - Color.White, + color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(bottomEnd = 20.dp, bottomStart = 20.dp), ) .padding(top = 13.dp, bottom = 5.dp), @@ -175,10 +163,12 @@ fun MenuScreen( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) .padding(top = 10.dp, start = 20.dp), ) { Text( text = stringResource(id = R.string.menu_my_liked_festival), + color = MaterialTheme.colorScheme.onBackground, style = Title3, ) TextButton( @@ -187,7 +177,7 @@ fun MenuScreen( ) { Text( text = stringResource(id = R.string.menu_add), - color = Color(0xFF545454), + color = MaterialTheme.colorScheme.surfaceVariant, style = Content7, textDecoration = TextDecoration.Underline, ) @@ -214,20 +204,26 @@ fun MenuScreen( } } } - item { UnifestHorizontalDivider() } + item { + HorizontalDivider( + thickness = 8.dp, + color = MaterialTheme.colorScheme.outline, + ) + } item { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) .padding(start = 20.dp, top = 10.dp), ) { Text( text = stringResource(id = R.string.menu_liked_booth), - style = Title3, - color = Color(0xFF161616), + color = MaterialTheme.colorScheme.onBackground, fontWeight = FontWeight.Bold, + style = Title3, ) TextButton( onClick = { onMenuUiAction(MenuUiAction.OnShowMoreClick) }, @@ -235,7 +231,7 @@ fun MenuScreen( ) { Text( text = stringResource(id = R.string.menu_watch_more), - color = Color(0xFF545454), + color = MaterialTheme.colorScheme.surfaceVariant, style = Content7, textDecoration = TextDecoration.Underline, ) @@ -255,25 +251,32 @@ fun MenuScreen( items = menuUiState.likedBooths.take(3), key = { _, booth -> booth.id }, ) { index, booth -> + Modifier + .clickable { + onMenuUiAction(MenuUiAction.OnLikedBoothItemClick(booth.id)) + } LikedBoothItem( booth = booth, index = index, totalCount = menuUiState.likedBooths.size, deleteLikedBooth = { onMenuUiAction(MenuUiAction.OnToggleBookmark(booth)) }, - modifier = Modifier - .clickable { - onMenuUiAction(MenuUiAction.OnLikedBoothItemClick(booth.id)) - } - .animateItemPlacement( - animationSpec = tween( - durationMillis = 500, - easing = LinearOutSlowInEasing, - ), + modifier = Modifier.animateItem( + fadeInSpec = null, + fadeOutSpec = null, + placementSpec = tween( + durationMillis = 500, + easing = LinearOutSlowInEasing, ), + ), ) } } - item { UnifestHorizontalDivider() } + item { + HorizontalDivider( + thickness = 8.dp, + color = MaterialTheme.colorScheme.outline, + ) + } item { MenuItem( icon = ImageVector.vectorResource(R.drawable.ic_inquiry), @@ -284,7 +287,7 @@ fun MenuScreen( item { HorizontalDivider( thickness = 1.dp, - color = Color(0xFFDFDFDF), + color = MaterialTheme.colorScheme.outline, ) } item { @@ -299,7 +302,7 @@ fun MenuScreen( item { HorizontalDivider( thickness = 1.dp, - color = Color(0xFFDFDFDF), + color = MaterialTheme.colorScheme.outline, ) } item { @@ -312,7 +315,7 @@ fun MenuScreen( Text( text = "UniFest v$appVersion", textAlign = TextAlign.Center, - color = Color(0xFFC5C5C5), + color = MaterialTheme.colorScheme.onSecondaryContainer, ) } } @@ -333,7 +336,7 @@ fun MenuScreen( if (festivalUiState.isFestivalSearchBottomSheetVisible) { FestivalSearchBottomSheet( searchText = festivalUiState.festivalSearchText, - searchTextHintRes = R.string.festival_search_text_field_hint, + searchTextHintRes = designR.string.festival_search_text_field_hint, likedFestivals = festivalUiState.likedFestivals, festivalSearchResults = festivalUiState.festivalSearchResults, isLikedFestivalDeleteDialogVisible = festivalUiState.isLikedFestivalDeleteDialogVisible, @@ -345,132 +348,16 @@ fun MenuScreen( } } -@Composable -fun FestivalItem( - festival: FestivalModel, - onMenuUiAction: (MenuUiAction) -> Unit, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .padding(vertical = 10.dp) - .clickable { - onMenuUiAction(MenuUiAction.OnLikedFestivalItemClick(festival.schoolName)) - }, - ) { - Box( - modifier = Modifier - .size(65.dp) - .shadow( - elevation = 6.dp, - shape = CircleShape, - ) - .background(Color.White, CircleShape) - .padding(5.dp), - contentAlignment = Alignment.Center, - ) { - NetworkImage( - imgUrl = festival.thumbnail, - contentDescription = "Festival Thumbnail", - modifier = Modifier - .size(60.dp) - .clip(CircleShape), - placeholder = painterResource(id = R.drawable.ic_item_placeholder), - ) - } - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = festival.schoolName, - color = Color(0xFF545454), - style = Content6, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = festival.festivalName, - color = Color.Black, - style = MenuTitle, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -fun MenuItem( - icon: ImageVector, - title: String, - onClick: () -> Unit, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(25.dp), - ) { - Icon( - imageVector = icon, - contentDescription = "Menu Icon", - tint = Color.Unspecified, - ) - Spacer(modifier = Modifier.width(16.dp)) - Text(title, style = Content8) - } -} - @DevicePreview @Composable -fun MenuScreenPreview() { +fun MenuScreenPreview( + @PreviewParameter(MenuPreviewParameterProvider::class) + menuUiState: MenuUiState, +) { UnifestTheme { MenuScreen( padding = PaddingValues(), - menuUiState = MenuUiState( - festivals = persistentListOf( - FestivalModel( - 1, - 1, - "https://picsum.photos/36", - "서울대학교", - "서울", - "설대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - FestivalModel( - 2, - 2, - "https://picsum.photos/36", - "연세대학교", - "서울", - "연대축제", - "2024-04-21", - "2024-04-23", - 126.957f, - 37.460f, - ), - ), - likedBooths = persistentListOf( - LikedBoothModel( - id = 1, - name = "부스 이름", - category = "부스 카테고리", - description = "부스 설명", - location = "부스 위치", - warning = "학과 전용 부스", - ), - LikedBoothModel( - id = 2, - name = "부스 이름", - category = "부스 카테고리", - description = "부스 설명", - location = "부스 위치", - warning = "학과 전용 부스", - ), - ), - ), + menuUiState = menuUiState, festivalUiState = FestivalUiState(), appVersion = "1.0.0", onMenuUiAction = {}, diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/component/FestivalItem.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/component/FestivalItem.kt new file mode 100644 index 00000000..f4b0e9ae --- /dev/null +++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/component/FestivalItem.kt @@ -0,0 +1,79 @@ +package com.unifest.android.feature.menu.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.R +import com.unifest.android.core.designsystem.component.NetworkImage +import com.unifest.android.core.designsystem.theme.Content6 +import com.unifest.android.core.designsystem.theme.MenuTitle +import com.unifest.android.core.model.FestivalModel +import com.unifest.android.feature.menu.viewmodel.MenuUiAction + +@Composable +fun FestivalItem( + festival: FestivalModel, + onMenuUiAction: (MenuUiAction) -> Unit, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .padding(vertical = 10.dp) + .clickable { + onMenuUiAction(MenuUiAction.OnLikedFestivalItemClick(festival.schoolName)) + }, + ) { + Box( + modifier = Modifier + .size(65.dp) + .shadow( + elevation = 6.dp, + shape = CircleShape, + ) + .background(MaterialTheme.colorScheme.tertiaryContainer, CircleShape) + .padding(5.dp), + contentAlignment = Alignment.Center, + ) { + NetworkImage( + imgUrl = festival.thumbnail, + contentDescription = "Festival Thumbnail", + modifier = Modifier + .size(60.dp) + .clip(CircleShape), + placeholder = painterResource(id = R.drawable.item_placeholder), + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = festival.schoolName, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = Content6, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = festival.festivalName, + color = MaterialTheme.colorScheme.onBackground, + style = MenuTitle, + textAlign = TextAlign.Center, + ) + } +} diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/component/MenuItem.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/component/MenuItem.kt new file mode 100644 index 00000000..be8fb8e3 --- /dev/null +++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/component/MenuItem.kt @@ -0,0 +1,47 @@ +package com.unifest.android.feature.menu.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.theme.Content8 + +@Composable +fun MenuItem( + icon: ImageVector, + title: String, + onClick: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .clickable(onClick = onClick) + .padding(25.dp), + ) { + Icon( + imageVector = icon, + contentDescription = "Menu Icon", + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = title, + color = MaterialTheme.colorScheme.onBackground, + style = Content8, + ) + } +} diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/navigation/MenuNavigation.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/navigation/MenuNavigation.kt index 04e790b7..d3195c47 100644 --- a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/navigation/MenuNavigation.kt +++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/navigation/MenuNavigation.kt @@ -6,12 +6,11 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable import com.unifest.android.core.common.UiText +import com.unifest.android.core.navigation.MainTabRoute import com.unifest.android.feature.menu.MenuRoute -const val MENU_ROUTE = "menu_route" - fun NavController.navigateToMenu(navOptions: NavOptions) { - navigate(MENU_ROUTE, navOptions) + navigate(MainTabRoute.Menu, navOptions) } fun NavGraphBuilder.menuNavGraph( @@ -21,7 +20,7 @@ fun NavGraphBuilder.menuNavGraph( navigateToBoothDetail: (Long) -> Unit, onShowSnackBar: (UiText) -> Unit, ) { - composable(route = MENU_ROUTE) { + composable { MenuRoute( padding = padding, popBackStack = popBackStack, diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/preview/MenuPreviewParameterProvider.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/preview/MenuPreviewParameterProvider.kt new file mode 100644 index 00000000..fa9a36e8 --- /dev/null +++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/preview/MenuPreviewParameterProvider.kt @@ -0,0 +1,58 @@ +package com.unifest.android.feature.menu.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.unifest.android.core.model.FestivalModel +import com.unifest.android.core.model.LikedBoothModel +import com.unifest.android.feature.menu.viewmodel.MenuUiState +import kotlinx.collections.immutable.persistentListOf + +internal class MenuPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + MenuUiState( + festivals = persistentListOf( + FestivalModel( + 1, + 1, + "https://picsum.photos/36", + "서울대학교", + "서울", + "설대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + FestivalModel( + 2, + 2, + "https://picsum.photos/36", + "연세대학교", + "서울", + "연대축제", + "2024-04-21", + "2024-04-23", + 126.957f, + 37.460f, + ), + ), + likedBooths = persistentListOf( + LikedBoothModel( + id = 1, + name = "부스 이름", + category = "부스 카테고리", + description = "부스 설명", + location = "부스 위치", + warning = "학과 전용 부스", + ), + LikedBoothModel( + id = 2, + name = "부스 이름", + category = "부스 카테고리", + description = "부스 설명", + location = "부스 위치", + warning = "학과 전용 부스", + ), + ), + ), + ) +} diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuViewModel.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuViewModel.kt index cc538e5f..4a3a59ea 100644 --- a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuViewModel.kt +++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuViewModel.kt @@ -9,7 +9,7 @@ import com.unifest.android.core.data.repository.BoothRepository import com.unifest.android.core.data.repository.FestivalRepository import com.unifest.android.core.data.repository.LikedBoothRepository import com.unifest.android.core.data.repository.LikedFestivalRepository -import com.unifest.android.core.designsystem.R +import com.unifest.android.core.designsystem.R as designR import com.unifest.android.core.model.LikedBoothModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList @@ -140,9 +140,9 @@ class MenuViewModel @Inject constructor( updateLikedBooth(booth) delay(500) getLikedBooths() - _uiEvent.send(MenuUiEvent.ShowSnackBar(UiText.StringResource(R.string.liked_booth_removed_message))) + _uiEvent.send(MenuUiEvent.ShowSnackBar(UiText.StringResource(designR.string.liked_booth_removed_message))) }.onFailure { - _uiEvent.send(MenuUiEvent.ShowSnackBar(UiText.StringResource(R.string.liked_booth_removed_failed_message))) + _uiEvent.send(MenuUiEvent.ShowSnackBar(UiText.StringResource(designR.string.liked_booth_removed_failed_message))) } } } @@ -186,7 +186,7 @@ class MenuViewModel @Inject constructor( // likedFestivalRepository.setRecentLikedFestival(schoolName) _uiEvent.send(MenuUiEvent.NavigateBack) } else { - _uiEvent.send(MenuUiEvent.ShowToast(UiText.StringResource(R.string.menu_interest_festival_snack_bar))) + _uiEvent.send(MenuUiEvent.ShowToast(UiText.StringResource(designR.string.interest_festival_snack_bar))) } } } diff --git a/feature/menu/src/main/res/drawable-night/ic_admin_mode.xml b/feature/menu/src/main/res/drawable-night/ic_admin_mode.xml new file mode 100644 index 00000000..fa848904 --- /dev/null +++ b/feature/menu/src/main/res/drawable-night/ic_admin_mode.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_inquiry.xml b/feature/menu/src/main/res/drawable-night/ic_inquiry.xml similarity index 89% rename from core/designsystem/src/main/res/drawable/ic_inquiry.xml rename to feature/menu/src/main/res/drawable-night/ic_inquiry.xml index 7ff3a4a4..edb9d9cd 100644 --- a/core/designsystem/src/main/res/drawable/ic_inquiry.xml +++ b/feature/menu/src/main/res/drawable-night/ic_inquiry.xml @@ -5,10 +5,10 @@ android:viewportHeight="24"> + android:fillColor="#B6B8C1"/> + android:strokeColor="#B6B8C1"/> diff --git a/core/designsystem/src/main/res/drawable/ic_admin_mode.xml b/feature/menu/src/main/res/drawable/ic_admin_mode.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_admin_mode.xml rename to feature/menu/src/main/res/drawable/ic_admin_mode.xml diff --git a/feature/menu/src/main/res/drawable/ic_inquiry.xml b/feature/menu/src/main/res/drawable/ic_inquiry.xml new file mode 100644 index 00000000..0f0cc657 --- /dev/null +++ b/feature/menu/src/main/res/drawable/ic_inquiry.xml @@ -0,0 +1,14 @@ + + + + diff --git a/feature/menu/src/main/res/values/strings.xml b/feature/menu/src/main/res/values/strings.xml new file mode 100644 index 00000000..474164c7 --- /dev/null +++ b/feature/menu/src/main/res/values/strings.xml @@ -0,0 +1,11 @@ + + + 메뉴 + 나의 관심 축제 + 추가하기 > + 관심 부스 + 더보기 > + 이용 문의 + 운영자 모드 진입 + + diff --git a/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/IntroNavigator.kt b/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/IntroNavigator.kt new file mode 100644 index 00000000..0cddf688 --- /dev/null +++ b/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/IntroNavigator.kt @@ -0,0 +1,3 @@ +package com.unifest.android.feature.navigator + +interface IntroNavigator : Navigator diff --git a/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/MainNavigator.kt b/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/MainNavigator.kt new file mode 100644 index 00000000..b5ef2435 --- /dev/null +++ b/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/MainNavigator.kt @@ -0,0 +1,3 @@ +package com.unifest.android.feature.navigator + +interface MainNavigator : Navigator diff --git a/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/Navigator.kt b/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/Navigator.kt similarity index 86% rename from feature/navigator/src/main/kotlin/com/unifest/feature/navigator/Navigator.kt rename to feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/Navigator.kt index d23ed9ce..164d3c9e 100644 --- a/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/Navigator.kt +++ b/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/Navigator.kt @@ -1,4 +1,4 @@ -package com.unifest.feature.navigator +package com.unifest.android.feature.navigator import android.app.Activity import android.content.Intent diff --git a/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/SplashNavigator.kt b/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/SplashNavigator.kt new file mode 100644 index 00000000..fe2216cb --- /dev/null +++ b/feature/navigator/src/main/kotlin/com/unifest/android/feature/navigator/SplashNavigator.kt @@ -0,0 +1,3 @@ +package com.unifest.android.feature.navigator + +interface SplashNavigator : Navigator diff --git a/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/IntroNavigator.kt b/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/IntroNavigator.kt deleted file mode 100644 index a08a74ea..00000000 --- a/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/IntroNavigator.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.unifest.feature.navigator - -interface IntroNavigator : Navigator diff --git a/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/MainNavigator.kt b/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/MainNavigator.kt deleted file mode 100644 index 552f5ca2..00000000 --- a/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/MainNavigator.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.unifest.feature.navigator - -interface MainNavigator : Navigator diff --git a/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/SplashNavigator.kt b/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/SplashNavigator.kt deleted file mode 100644 index df82cd19..00000000 --- a/feature/navigator/src/main/kotlin/com/unifest/feature/navigator/SplashNavigator.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.unifest.feature.navigator - -interface SplashNavigator : Navigator diff --git a/feature/splash/build.gradle.kts b/feature/splash/build.gradle.kts index 2b200059..28758982 100644 --- a/feature/splash/build.gradle.kts +++ b/feature/splash/build.gradle.kts @@ -15,6 +15,17 @@ android { defaultConfig { buildConfigField("String", "PACKAGE_NAME", "\"${libs.versions.packageName.get()}\"") } + + android { + buildTypes { + debug { + isMinifyEnabled = false + } + release { + isMinifyEnabled = false + } + } + } } dependencies { diff --git a/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/SplashActivity.kt b/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/SplashActivity.kt index 66c69b6b..2be20bcf 100644 --- a/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/SplashActivity.kt +++ b/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/SplashActivity.kt @@ -4,12 +4,14 @@ import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.graphics.Color import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import com.unifest.android.core.designsystem.theme.DarkGrey100 import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.feature.navigator.IntroNavigator -import com.unifest.feature.navigator.MainNavigator +import com.unifest.android.feature.navigator.IntroNavigator +import com.unifest.android.feature.navigator.MainNavigator import dagger.hilt.android.AndroidEntryPoint import tech.thdev.compose.exteions.system.ui.controller.rememberExSystemUiController import javax.inject.Inject @@ -28,11 +30,12 @@ class SplashActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { val systemUiController = rememberExSystemUiController() + val isDarkTheme = isSystemInDarkTheme() DisposableEffect(systemUiController) { systemUiController.setSystemBarsColor( - color = Color.White, - darkIcons = true, + color = if (isDarkTheme) DarkGrey100 else Color.White, + darkIcons = !isDarkTheme, isNavigationBarContrastEnforced = false, ) diff --git a/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/SplashScreen.kt b/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/SplashScreen.kt index da02652e..29adcb38 100644 --- a/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/SplashScreen.kt +++ b/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/SplashScreen.kt @@ -4,11 +4,11 @@ import android.content.Intent import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat.startActivity import androidx.hilt.navigation.compose.hiltViewModel @@ -36,7 +36,7 @@ internal fun SplashRoute( LaunchedEffect(key1 = shouldUpdate) { if (shouldUpdate == false) { - viewModel.checkIntroCompletion() + viewModel.refreshFCMToken() } } @@ -82,7 +82,7 @@ fun SplashScreen( LoadingWheel( modifier = Modifier .fillMaxSize() - .background(Color.White), + .background(MaterialTheme.colorScheme.background), ) } if (shouldUpdate == true) { @@ -96,7 +96,7 @@ fun SplashScreen( LoadingWheel( modifier = Modifier .fillMaxSize() - .background(Color.White), + .background(MaterialTheme.colorScheme.background), ) } } diff --git a/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/di/SplashNavigator.kt b/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/di/SplashNavigator.kt index 575b2c35..f3d736eb 100644 --- a/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/di/SplashNavigator.kt +++ b/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/di/SplashNavigator.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.content.Intent import com.unifest.android.core.common.extension.startActivityWithAnimation import com.unifest.android.feature.splash.SplashActivity -import com.unifest.feature.navigator.SplashNavigator +import com.unifest.android.feature.navigator.SplashNavigator import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/viewmodel/SplashViewModel.kt b/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/viewmodel/SplashViewModel.kt index cc19b155..691a936e 100644 --- a/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/viewmodel/SplashViewModel.kt +++ b/feature/splash/src/main/kotlin/com/unifest/android/feature/splash/viewmodel/SplashViewModel.kt @@ -2,7 +2,8 @@ package com.unifest.android.feature.splash.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.unifest.android.core.data.repository.OnboardingRepository +import com.unifest.android.core.data.repository.LikedFestivalRepository +import com.unifest.android.core.data.repository.MessagingRepository import com.unifest.android.core.data.repository.RemoteConfigRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel @@ -12,11 +13,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( - private val onboardingRepository: OnboardingRepository, + // private val onboardingRepository: OnboardingRepository, + private val likedFestivalRepository: LikedFestivalRepository, + private val messagingRepository: MessagingRepository, remoteConfigRepository: RemoteConfigRepository, ) : ViewModel() { private val _uiState = MutableStateFlow(SplashUiState()) @@ -34,12 +38,38 @@ class SplashViewModel @Inject constructor( } } - fun checkIntroCompletion() { +// // 학교를 하나만 서비스 하기 때문에 Intro 스킵 +// fun checkIntroCompletion() { +// viewModelScope.launch { +// if (onboardingRepository.checkIntroCompletion()) { +// _uiEvent.send(SplashUiEvent.NavigateToMain) +// } else { +// _uiEvent.send(SplashUiEvent.NavigateToIntro) +// } +// } +// } + + private fun setRecentLikedFestival() { + viewModelScope.launch { + likedFestivalRepository.setRecentLikedFestival("한경대") + likedFestivalRepository.setRecentLikedFestivalId(2L) + _uiEvent.send(SplashUiEvent.NavigateToMain) + } + } + + @Suppress("TooGenericExceptionCaught") + fun refreshFCMToken() { viewModelScope.launch { - if (onboardingRepository.checkIntroCompletion()) { - _uiEvent.send(SplashUiEvent.NavigateToMain) - } else { - _uiEvent.send(SplashUiEvent.NavigateToIntro) + try { + val token = messagingRepository.refreshFCMToken() + token?.let { + Timber.d("New FCM token: $it") + messagingRepository.setFCMToken(it) + // 한경대로 고정 + setRecentLikedFestival() + } + } catch (e: Exception) { + Timber.e(e, "Error getting or saving FCM token") } } } diff --git a/feature/stamp/.gitignore b/feature/stamp/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/stamp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/stamp/build.gradle.kts b/feature/stamp/build.gradle.kts new file mode 100644 index 00000000..6313912d --- /dev/null +++ b/feature/stamp/build.gradle.kts @@ -0,0 +1,33 @@ +@file:Suppress("INLINE_FROM_HIGHER_PLATFORM") + +plugins { + alias(libs.plugins.unifest.android.feature) + alias(libs.plugins.kotlin.serialization) + // alias(libs.plugins.compose.investigator) +} + +android { + namespace = "com.unifest.android.feature.stamp" + + android { + buildTypes { + debug { + isMinifyEnabled = false + } + release { + isMinifyEnabled = false + } + } + } +} + +dependencies { + implementations( + projects.core.data, + + libs.kotlinx.collections.immutable, + libs.coil.compose, + libs.zxing.android.embedded, + libs.timber, + ) +} diff --git a/feature/stamp/src/main/AndroidManifest.xml b/feature/stamp/src/main/AndroidManifest.xml new file mode 100644 index 00000000..35ee9ccb --- /dev/null +++ b/feature/stamp/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/QRScanActivity.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/QRScanActivity.kt new file mode 100644 index 00000000..fed798e1 --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/QRScanActivity.kt @@ -0,0 +1,89 @@ +package com.unifest.android.feature.stamp + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.KeyEvent +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.google.zxing.BarcodeFormat +import com.journeyapps.barcodescanner.BarcodeCallback +import com.journeyapps.barcodescanner.BarcodeResult +import com.journeyapps.barcodescanner.DecoratedBarcodeView +import com.journeyapps.barcodescanner.DefaultDecoderFactory +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.feature.stamp.viewmodel.QRScanViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class QRScanActivity : ComponentActivity() { + private val barcodeView: DecoratedBarcodeView by lazy { + DecoratedBarcodeView(this).apply { + barcodeView.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE)) + initializeFromIntent(intent) + decodeContinuous(callback) + statusView.isVisible = false + } + } + + private val viewModel: QRScanViewModel by viewModels() + + private val callback = BarcodeCallback { result: BarcodeResult -> + result.text ?: return@BarcodeCallback + viewModel.scan(result.text) + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + barcodeView.pause() + delay(1000) + barcodeView.resume() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + UnifestTheme { + QRScanScreen( + barcodeView = barcodeView, + popBackStack = { finish() }, + onAction = viewModel::onAction, + ) + } + } + } + + override fun onResume() { + super.onResume() + if (checkCameraPermission()) { + barcodeView.resume() + } else { + finish() + } + } + + override fun onPause() { + super.onPause() + barcodeView.pause() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + return barcodeView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event) + } + + private fun checkCameraPermission(): Boolean { + return ContextCompat.checkSelfPermission( + this, + Manifest.permission.CAMERA, + ) == PackageManager.PERMISSION_GRANTED + } +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/QRScanScreen.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/QRScanScreen.kt new file mode 100644 index 00000000..b2bebdea --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/QRScanScreen.kt @@ -0,0 +1,152 @@ +package com.unifest.android.feature.stamp + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.hilt.navigation.compose.hiltViewModel +import com.journeyapps.barcodescanner.DecoratedBarcodeView +import com.unifest.android.core.common.ObserveAsEvents +import com.unifest.android.core.common.extension.findActivity +import com.unifest.android.core.designsystem.component.UnifestScaffold +import com.unifest.android.core.designsystem.theme.BoothTitle2 +import com.unifest.android.core.designsystem.theme.QRDescription +import com.unifest.android.core.designsystem.theme.Title0 +import com.unifest.android.feature.stamp.viewmodel.QRErrorType +import com.unifest.android.feature.stamp.viewmodel.QRScanUiAction +import com.unifest.android.feature.stamp.viewmodel.QRScanUiEvent +import com.unifest.android.feature.stamp.viewmodel.QRScanViewModel +import com.unifest.android.core.designsystem.R as designR + +@Composable +fun QRScanScreen( + barcodeView: DecoratedBarcodeView, + popBackStack: () -> Unit, + onAction: (QRScanUiAction) -> Unit, + viewModel: QRScanViewModel = hiltViewModel(), +) { + val context = LocalContext.current + val activity = context.findActivity() + + LaunchedEffect(barcodeView) { + barcodeView.resume() + } + + ObserveAsEvents(flow = viewModel.uiEvent) { event -> + when (event) { + is QRScanUiEvent.NavigateBack -> popBackStack() + is QRScanUiEvent.ScanError -> { + when (event.errorType) { + QRErrorType.ShowNotToday -> Toast.makeText(context, "", Toast.LENGTH_SHORT).show() + QRErrorType.UsedTicket -> Toast.makeText(context, "", Toast.LENGTH_SHORT).show() + QRErrorType.TicketNotFound -> Toast.makeText(context, "", Toast.LENGTH_SHORT).show() + } + } + + is QRScanUiEvent.ScanSuccess -> Toast.makeText(context, "", Toast.LENGTH_SHORT).show() + is QRScanUiEvent.ShowToast -> { + Toast.makeText(context, event.text.asString(context), Toast.LENGTH_SHORT).show() + activity.finish() + } + } + } + + UnifestScaffold( + topBar = { + QRTopBar(onAction = onAction) + }, + bottomBar = { + QRScanBottomBar() + }, + ) { innerPadding -> + AndroidView( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + factory = { barcodeView }, + ) + } +} + +@Composable +private fun QRTopBar( + onAction: (QRScanUiAction) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(21.dp)) + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_arrow_back_dark_gray), + contentDescription = "Arrow Back Icon", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.clickable { onAction(QRScanUiAction.OnBackClick) }, + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(id = R.string.stamp_qr_scan_title), + modifier = Modifier.padding(vertical = 18.dp), + color = MaterialTheme.colorScheme.onBackground, + style = BoothTitle2, + ) + } + Spacer(modifier = Modifier.height(40.dp)) + } +} + +@Composable +private fun QRScanBottomBar( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(92.dp)) + Text( + text = stringResource(R.string.stamp_qr_scan_description1), + color = MaterialTheme.colorScheme.onBackground, + style = Title0, + ) + Text( + text = stringResource(R.string.stamp_qr_scan_description2), + color = MaterialTheme.colorScheme.onBackground, + style = QRDescription, + ) + Spacer(modifier = Modifier.height(110.dp)) + } + } +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/StampScreen.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/StampScreen.kt new file mode 100644 index 00000000..66ab24e8 --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/StampScreen.kt @@ -0,0 +1,336 @@ +package com.unifest.android.feature.stamp + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.startActivity +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.unifest.android.core.common.ObserveAsEvents +import com.unifest.android.core.common.PermissionDialogButtonType +import com.unifest.android.core.common.extension.findActivity +import com.unifest.android.core.designsystem.theme.BoothTitle2 +import com.unifest.android.core.designsystem.theme.Content1 +import com.unifest.android.core.designsystem.theme.Content2 +import com.unifest.android.core.designsystem.theme.DarkGrey200 +import com.unifest.android.core.designsystem.theme.DarkGrey400 +import com.unifest.android.core.designsystem.theme.LightGrey100 +import com.unifest.android.core.designsystem.theme.MenuTitle +import com.unifest.android.core.designsystem.theme.StampCount +import com.unifest.android.core.designsystem.theme.Title1 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.ui.DevicePreview +import com.unifest.android.core.ui.component.CameraPermissionTextProvider +import com.unifest.android.core.ui.component.PermissionDialog +import com.unifest.android.feature.stamp.component.StampBoothBottomSheet +import com.unifest.android.feature.stamp.component.StampButton +import com.unifest.android.feature.stamp.preview.StampPreviewParameterProvider +import com.unifest.android.feature.stamp.viewmodel.StampUiAction +import com.unifest.android.feature.stamp.viewmodel.StampUiEvent +import com.unifest.android.feature.stamp.viewmodel.StampUiState +import com.unifest.android.feature.stamp.viewmodel.StampViewModel + +@Composable +internal fun StampRoute( + padding: PaddingValues, + popBackStack: () -> Unit, + navigateToBoothDetail: (Long) -> Unit, + viewModel: StampViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current + val activity = context.findActivity() + + var isCameraPermissionGranted by remember { + mutableStateOf( + activity.checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED, + ) + } + + val permissionResultLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { isGranted -> + isCameraPermissionGranted = isGranted + viewModel.onPermissionResult(isGranted) + }, + ) + + val settingsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + onResult = { + // 설정에서 돌아왔을 때 권한 상태를 다시 확인 + isCameraPermissionGranted = activity.checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED + viewModel.onPermissionResult(isCameraPermissionGranted) + }, + ) + + ObserveAsEvents(flow = viewModel.uiEvent) { event -> + when (event) { + is StampUiEvent.NavigateBack -> popBackStack() + is StampUiEvent.NavigateToQRScan -> startActivity(context, Intent(context, QRScanActivity::class.java), null) + is StampUiEvent.RequestCameraPermission -> permissionResultLauncher.launch(Manifest.permission.CAMERA) + is StampUiEvent.NavigateToAppSetting -> { + val intent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", activity.packageName, null), + ) + settingsLauncher.launch(intent) + } + + is StampUiEvent.NavigateToBoothDetail -> navigateToBoothDetail(event.boothId) + } + } + + StampScreen( + padding = padding, + uiState = uiState, + onAction = viewModel::onAction, + ) +} + +@Composable +internal fun StampScreen( + padding: PaddingValues, + uiState: StampUiState, + onAction: (StampUiAction) -> Unit, +) { + val activity = LocalContext.current.findActivity() + +// val checkedStampPainter = rememberAsyncImagePainter(R.drawable.ic_checked_stamp) +// val uncheckedStampPainter = rememberAsyncImagePainter(R.drawable.ic_unchecked_stamp) + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(padding), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(20.dp)) + Text( + text = stringResource(id = R.string.stamp_title), + modifier = Modifier.padding(vertical = 18.dp), + color = MaterialTheme.colorScheme.onBackground, + style = BoothTitle2, + ) + } + Spacer(modifier = Modifier.height(6.dp)) + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 20.dp) + .background(color = if (isSystemInDarkTheme()) DarkGrey200 else LightGrey100, shape = RoundedCornerShape(10.dp)), + ) { + Column { + Spacer(modifier = Modifier.height(26.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(24.dp)) + Column { + Text( + text = "한국교통대학교", + color = MaterialTheme.colorScheme.onBackground, + style = Title1, + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = stringResource(id = R.string.stamp_collection_status), + color = MaterialTheme.colorScheme.onSurface, + style = Content1, + ) + } + Spacer(modifier = Modifier.weight(1f)) + StampButton( + onClick = { + onAction(StampUiAction.OnReceiveStampClick) + }, + text = stringResource(id = R.string.receive_stamp), + ) + Spacer(modifier = Modifier.width(16.dp)) + } + Spacer(modifier = Modifier.height(21.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(24.dp)) + Text( + text = buildAnnotatedString { + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.onBackground)) { + append("${uiState.receivedStamp}") + } + withStyle(style = SpanStyle(color = MaterialTheme.colorScheme.onSurface)) { + append(" / ${uiState.stampBoothList.size} 개") + } + }, + style = StampCount, + ) + Spacer(modifier = Modifier.weight(1f)) + Row( + modifier = Modifier.clickable { + onAction(StampUiAction.OnRefreshClick) + }, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.refresh), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = Content2, + ) + Spacer(modifier = Modifier.width(4.dp)) + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_refresh), + contentDescription = "refresh icon", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(modifier = Modifier.width(24.dp)) + } + Spacer(modifier = Modifier.height(44.dp)) + LazyVerticalGrid( + columns = GridCells.Fixed(4), + modifier = Modifier + .padding(horizontal = 24.dp) + .height(if (uiState.stampBoothList.isEmpty()) 0.dp else (((uiState.stampBoothList.size - 1) / 4 + 1) * 84).dp), + verticalArrangement = Arrangement.spacedBy(11.dp), + horizontalArrangement = Arrangement.spacedBy(9.dp), + ) { + items( + count = uiState.stampBoothList.size, + key = { index -> uiState.stampBoothList[index].id }, + ) { index -> + Box { + Image( + painter = if (uiState.stampBoothList[index].isChecked) painterResource(id = R.drawable.ic_checked_stamp) + else painterResource(id = R.drawable.ic_unchecked_stamp), + contentDescription = "stamp image", + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(10.dp)), + ) + } + } + } + Spacer(modifier = Modifier.height(54.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .height(50.dp) + .padding(horizontal = 24.dp) + .background(color = if (isSystemInDarkTheme()) DarkGrey400 else Color.White, shape = RoundedCornerShape(7.dp)) + .clickable { + onAction(StampUiAction.OnFindStampBoothClick) + }, + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(24.dp)) + Text( + text = stringResource(id = R.string.find_stamp_booth), + color = MaterialTheme.colorScheme.onSecondary, + style = MenuTitle, + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_right), + contentDescription = "arrow right icon", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.width(22.dp)) + } + Spacer(modifier = Modifier.height(21.dp)) + } + } + } + + if (uiState.isPermissionDialogVisible) { + PermissionDialog( + permissionTextProvider = CameraPermissionTextProvider(), + isPermanentlyDeclined = !activity.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA), + onDismiss = { onAction(StampUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.DISMISS)) }, + navigateToAppSetting = { onAction(StampUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.NAVIGATE_TO_APP_SETTING)) }, + onConfirm = { onAction(StampUiAction.OnPermissionDialogButtonClick(PermissionDialogButtonType.CONFIRM)) }, + ) + } + + if (uiState.isStampBoothDialogVisible) { + StampBoothBottomSheet( + schoolName = uiState.schoolName, + stampBoothList = uiState.stampBoothList, + onAction = onAction, + ) + } + } +} + +@DevicePreview +@Composable +fun StampScreenPreview( + @PreviewParameter(StampPreviewParameterProvider::class) + stampUiState: StampUiState, +) { + UnifestTheme { + StampScreen( + padding = PaddingValues(), + uiState = stampUiState, + onAction = {}, + ) + } +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/component/StampBoothBottomSheet.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/component/StampBoothBottomSheet.kt new file mode 100644 index 00000000..d4b486a9 --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/component/StampBoothBottomSheet.kt @@ -0,0 +1,139 @@ +package com.unifest.android.feature.stamp.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.theme.Content1 +import com.unifest.android.core.designsystem.theme.Title1 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.model.StampBoothModel +import com.unifest.android.core.designsystem.theme.Content2 +import com.unifest.android.feature.stamp.R +import com.unifest.android.feature.stamp.viewmodel.StampUiAction +import com.unifest.android.feature.stamp.viewmodel.StampUiState +import kotlinx.collections.immutable.ImmutableList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun StampBoothBottomSheet( + schoolName: String, + stampBoothList: ImmutableList, + onAction: (StampUiAction) -> Unit, +) { + val bottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { it != SheetValue.Hidden }, + ) + + ModalBottomSheet( + onDismissRequest = { + onAction(StampUiAction.OnDismiss) + }, + sheetState = bottomSheetState, + shape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp), + containerColor = MaterialTheme.colorScheme.surface, + dragHandle = { + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(top = 10.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + HorizontalDivider( + thickness = 5.dp, + color = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier + .width(80.dp) + .clip(RoundedCornerShape(43.dp)), + ) + } + }, + windowInsets = WindowInsets(top = 0), + modifier = Modifier + .fillMaxHeight() + .background(MaterialTheme.colorScheme.surface) + .padding(top = 18.dp), + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .navigationBarsPadding(), + ) { + item { + Column( + modifier = Modifier.padding(horizontal = 30.dp), + ) { + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = schoolName, + color = MaterialTheme.colorScheme.onSurface, + style = Content1, + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = stringResource(id = R.string.stamp_booth), + color = MaterialTheme.colorScheme.onBackground, + style = Title1, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "총 ${stampBoothList.size}개", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = Content2, + ) + Spacer(modifier = Modifier.height(12.dp)) + } + } + items( + count = stampBoothList.size, + key = { index -> stampBoothList[index].id }, + ) { index -> + StampBoothItem( + stampBooth = stampBoothList[index], + modifier = Modifier.clickable { + onAction(StampUiAction.OnStampBoothItemClick(stampBoothList[index].id)) + }, + ) + } + } + } +} + +@ComponentPreview +@Composable +fun SchoolSearchBottomSheetPreview() { + UnifestTheme { + StampBoothBottomSheet( + schoolName = "", + stampBoothList = StampUiState().stampBoothList, + onAction = {}, + ) + } +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/component/StampBoothItem.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/component/StampBoothItem.kt new file mode 100644 index 00000000..8e8491bd --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/component/StampBoothItem.kt @@ -0,0 +1,111 @@ +package com.unifest.android.feature.stamp.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.R as designR +import com.unifest.android.core.designsystem.component.NetworkImage +import com.unifest.android.core.designsystem.theme.Content2 +import com.unifest.android.core.designsystem.theme.Title2 +import com.unifest.android.core.designsystem.theme.Title5 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.model.StampBoothModel + +@Composable +fun StampBoothItem( + stampBooth: StampBoothModel, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 20.dp), + ) { + Spacer(modifier = Modifier.height(16.dp)) + Row { + NetworkImage( + imgUrl = stampBooth.thumbnail, + contentDescription = "Stamp Booth Thumbnail", + modifier = Modifier + .size(86.dp) + .clip(RoundedCornerShape(16.dp)), + placeholder = painterResource(id = designR.drawable.item_placeholder), + ) + Spacer(modifier = Modifier.width(14.dp)) + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { + Text( + text = stampBooth.name, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = Title2, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stampBooth.description, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = Content2, + ) + Spacer(modifier = Modifier.height(6.dp)) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_location_green), + contentDescription = "Location Icon", + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(3.dp)) + Text( + text = stampBooth.location, + modifier = Modifier.align(Alignment.CenterVertically), + color = MaterialTheme.colorScheme.onSecondary, + style = Title5, + ) + } + } + } + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@ComponentPreview +@Composable +private fun StampBoothItemPreview() { + UnifestTheme { + StampBoothItem( + stampBooth = StampBoothModel( + id = 1, + name = "부스 이름", + category = "부스 카테고리", + description = "부스 설명", + location = "부스 위치", + ), + ) + } +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/component/StampButton.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/component/StampButton.kt new file mode 100644 index 00000000..03b296fb --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/component/StampButton.kt @@ -0,0 +1,67 @@ +package com.unifest.android.feature.stamp.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.unifest.android.core.common.utils.dpToPx +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.theme.Title2 +import com.unifest.android.core.designsystem.theme.UnifestTheme + +@Composable +internal fun StampButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val widthPx = dpToPx(196.dp) + val heightPx = dpToPx(78.dp) + + val gradient = Brush.linearGradient( + colorStops = arrayOf( + 0.0f to Color(0xF5FF8699), + 0.45f to Color(0xFFFF4264), + 1.0f to Color(0xFFEF39FF), + ), + start = Offset(0f, 0f), + end = Offset(widthPx, heightPx), + ) + + Button( + onClick = onClick, + modifier = modifier + .width(140.dp) + .height(52.dp) + .background(gradient, shape = RoundedCornerShape(26.dp)) + .then(Modifier.clip(RoundedCornerShape(26.dp))), + colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), + ) { + Text( + text = text, + color = Color.White, + style = Title2, + ) + } +} + +@ComponentPreview +@Composable +private fun StampButtonPreview() { + UnifestTheme { + StampButton( + text = "스탬프 받기", + onClick = {}, + ) + } +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/navigation/StampNavigation.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/navigation/StampNavigation.kt new file mode 100644 index 00000000..a404e5bc --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/navigation/StampNavigation.kt @@ -0,0 +1,27 @@ +package com.unifest.android.feature.stamp.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.unifest.android.core.navigation.MainTabRoute +import com.unifest.android.feature.stamp.StampRoute + +fun NavController.navigateToStamp(navOptions: NavOptions) { + navigate(MainTabRoute.Stamp, navOptions) +} + +fun NavGraphBuilder.stampNavGraph( + padding: PaddingValues, + popBackStack: () -> Unit, + navigateToBoothDetail: (Long) -> Unit, +) { + composable { + StampRoute( + padding = padding, + popBackStack = popBackStack, + navigateToBoothDetail = navigateToBoothDetail, + ) + } +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/preview/StampPreviewParameterProvider.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/preview/StampPreviewParameterProvider.kt new file mode 100644 index 00000000..9766e601 --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/preview/StampPreviewParameterProvider.kt @@ -0,0 +1,63 @@ +package com.unifest.android.feature.stamp.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.unifest.android.core.model.StampBoothModel +import com.unifest.android.feature.stamp.viewmodel.StampUiState +import kotlinx.collections.immutable.persistentListOf + +internal class StampPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + StampUiState( + stampBoothList = persistentListOf( + StampBoothModel( + id = 0, + isChecked = true, + ), + StampBoothModel( + id = 1, + isChecked = true, + ), + StampBoothModel( + id = 2, + isChecked = true, + ), + StampBoothModel( + id = 3, + isChecked = true, + ), + StampBoothModel( + id = 4, + isChecked = false, + ), + StampBoothModel( + id = 5, + isChecked = true, + ), + StampBoothModel( + id = 6, + isChecked = true, + ), + StampBoothModel( + id = 7, + isChecked = false, + ), + StampBoothModel( + id = 8, + isChecked = true, + ), + StampBoothModel( + id = 9, + isChecked = true, + ), + StampBoothModel( + id = 10, + isChecked = false, + ), + StampBoothModel( + id = 11, + isChecked = true, + ), + ), + ), + ) +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanUiAction.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanUiAction.kt new file mode 100644 index 00000000..74a5fc49 --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanUiAction.kt @@ -0,0 +1,5 @@ +package com.unifest.android.feature.stamp.viewmodel + +sealed interface QRScanUiAction { + data object OnBackClick : QRScanUiAction +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanUiEvent.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanUiEvent.kt new file mode 100644 index 00000000..522c3baa --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanUiEvent.kt @@ -0,0 +1,27 @@ +package com.unifest.android.feature.stamp.viewmodel + +import com.unifest.android.core.common.UiText + +sealed interface QRScanUiEvent { + data object NavigateBack : QRScanUiEvent + data object ScanSuccess : QRScanUiEvent + data class ScanError(val errorType: QRErrorType) : QRScanUiEvent + data class ShowToast(val text: UiText) : QRScanUiEvent +} + +data class QRScanException( + val errorType: QRErrorType?, +) : Exception(errorType?.name) + +enum class QRErrorType { + ShowNotToday, UsedTicket, TicketNotFound; + + companion object { + fun fromString(type: String?): QRErrorType? = when (type?.trim()?.uppercase()) { + "SHOW_NOT_TODAY" -> ShowNotToday + "USED_TICKET" -> UsedTicket + "TICKET_NOT_FOUND" -> TicketNotFound + else -> null + } + } +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanUiState.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanUiState.kt new file mode 100644 index 00000000..89cfb0e0 --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanUiState.kt @@ -0,0 +1,6 @@ +package com.unifest.android.feature.stamp.viewmodel + +data class QRScanUiState( + val festivalName: String = "", + val boothId: Long = 0L, +) diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanViewModel.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanViewModel.kt new file mode 100644 index 00000000..88cc2f37 --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/QRScanViewModel.kt @@ -0,0 +1,48 @@ +package com.unifest.android.feature.stamp.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.unifest.android.core.common.UiText +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class QRScanViewModel @Inject constructor() : ViewModel() { + private val _uiState = MutableStateFlow(QRScanUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _uiEvent = Channel() + val uiEvent: Flow = _uiEvent.receiveAsFlow() + + fun onAction(action: QRScanUiAction) { + when (action) { + is QRScanUiAction.OnBackClick -> navigateBack() + } + } + + private fun navigateBack() { + viewModelScope.launch { + _uiEvent.send(QRScanUiEvent.NavigateBack) + } + } + + /** + * 스캐너가 QR코드를 스캔하면 호출하는 함수 + * + * @param entryCode 스캔한 QR 의 데이터 + */ + fun scan(entryCode: String) { + Timber.tag("QRScanActivity").d("스캔 결과: $entryCode") + viewModelScope.launch { + _uiEvent.send(QRScanUiEvent.ShowToast(UiText.DirectString(entryCode))) + } + } +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampUiAction.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampUiAction.kt new file mode 100644 index 00000000..790db6a8 --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampUiAction.kt @@ -0,0 +1,12 @@ +package com.unifest.android.feature.stamp.viewmodel + +import com.unifest.android.core.common.PermissionDialogButtonType + +sealed interface StampUiAction { + data object OnReceiveStampClick : StampUiAction + data object OnFindStampBoothClick : StampUiAction + data object OnRefreshClick : StampUiAction + data class OnPermissionDialogButtonClick(val buttonType: PermissionDialogButtonType) : StampUiAction + data object OnDismiss : StampUiAction + data class OnStampBoothItemClick(val boothId: Long) : StampUiAction +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampUiEvent.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampUiEvent.kt new file mode 100644 index 00000000..0fc0d463 --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampUiEvent.kt @@ -0,0 +1,9 @@ +package com.unifest.android.feature.stamp.viewmodel + +sealed interface StampUiEvent { + data object NavigateBack : StampUiEvent + data object NavigateToQRScan : StampUiEvent + data object RequestCameraPermission : StampUiEvent + data object NavigateToAppSetting : StampUiEvent + data class NavigateToBoothDetail(val boothId: Long) : StampUiEvent +} diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampUiState.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampUiState.kt new file mode 100644 index 00000000..f497341a --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampUiState.kt @@ -0,0 +1,113 @@ +package com.unifest.android.feature.stamp.viewmodel + +import com.unifest.android.core.model.StampBoothModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class StampUiState( + val isLoading: Boolean = false, + val schoolName: String = "한국교통대학교", + val receivedStamp: Int = 9, + val stampBoothList: ImmutableList = persistentListOf( + StampBoothModel( + id = 0, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = true, + ), + StampBoothModel( + id = 1, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = true, + ), + StampBoothModel( + id = 2, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = true, + ), + StampBoothModel( + id = 3, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = true, + ), + StampBoothModel( + id = 4, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = true, + ), + StampBoothModel( + id = 5, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = false, + ), + StampBoothModel( + id = 6, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = true, + ), + StampBoothModel( + id = 7, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = true, + ), + StampBoothModel( + id = 8, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = false, + ), + StampBoothModel( + id = 9, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = true, + ), + StampBoothModel( + id = 10, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = true, + ), + StampBoothModel( + id = 11, + name = "컴공 주점 부스", + category = "주점", + description = "저희 주점은 일본 이자카야를 모티브로 만든 컴공인을 위한 주점입니다.", + location = "학생회관 옆", + isChecked = false, + ), + ), + val isStampBoothDialogVisible: Boolean = false, + val isPermissionDialogVisible: Boolean = false, + val isServerErrorDialogVisible: Boolean = false, + val isNetworkErrorDialogVisible: Boolean = false, +) diff --git a/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampViewModel.kt b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampViewModel.kt new file mode 100644 index 00000000..c3f02897 --- /dev/null +++ b/feature/stamp/src/main/kotlin/com/unifest/android/feature/stamp/viewmodel/StampViewModel.kt @@ -0,0 +1,117 @@ +package com.unifest.android.feature.stamp.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.unifest.android.core.common.ErrorHandlerActions +import com.unifest.android.core.common.PermissionDialogButtonType +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class StampViewModel @Inject constructor( + // private val stampRepository: StampRepository, +) : ViewModel(), ErrorHandlerActions { + private val _uiState = MutableStateFlow(StampUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _uiEvent = Channel() + val uiEvent: Flow = _uiEvent.receiveAsFlow() + + fun onAction(action: StampUiAction) { + when (action) { + is StampUiAction.OnReceiveStampClick -> requestLocationPermission() + is StampUiAction.OnRefreshClick -> refresh() + is StampUiAction.OnFindStampBoothClick -> setStampBoothDialogVisible(true) + is StampUiAction.OnPermissionDialogButtonClick -> handlePermissionDialogButtonClick(action.buttonType) + is StampUiAction.OnDismiss -> setStampBoothDialogVisible(false) + is StampUiAction.OnStampBoothItemClick -> navigateToBoothDetail(action.boothId) + } + } + + private fun setStampBoothDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isStampBoothDialogVisible = flag) + } + } + + private fun refresh() { + // API 재 호출 + } + + private fun requestLocationPermission() { + viewModelScope.launch { + _uiEvent.send(StampUiEvent.RequestCameraPermission) + } + } + + private fun navigateToQRScan() { + viewModelScope.launch { + _uiEvent.send(StampUiEvent.NavigateToQRScan) + } + } + + fun onPermissionResult(isGranted: Boolean) { + if (isGranted) { + setPermissionDialogVisible(false) + navigateToQRScan() + } else { + setPermissionDialogVisible(true) + } + } + + private fun handlePermissionDialogButtonClick(buttonType: PermissionDialogButtonType) { + when (buttonType) { + PermissionDialogButtonType.DISMISS -> { + setPermissionDialogVisible(false) + } + + PermissionDialogButtonType.NAVIGATE_TO_APP_SETTING -> { + viewModelScope.launch { + _uiEvent.send(StampUiEvent.NavigateToAppSetting) + } + } + + PermissionDialogButtonType.CONFIRM -> { + setPermissionDialogVisible(false) + viewModelScope.launch { + _uiEvent.send(StampUiEvent.RequestCameraPermission) + } + } + } + } + + private fun setPermissionDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isPermissionDialogVisible = flag) + } + } + + @Suppress("UnusedParameter") + private fun navigateToBoothDetail(boothId: Long) { + viewModelScope.launch { + // 임시 구현 + // _uiEvent.send(StampUiEvent.NavigateToBoothDetail(boothId)) + _uiEvent.send(StampUiEvent.NavigateToBoothDetail(79L)) + } + } + + override fun setServerErrorDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isServerErrorDialogVisible = flag) + } + } + + override fun setNetworkErrorDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isNetworkErrorDialogVisible = flag) + } + } +} diff --git a/feature/stamp/src/main/res/drawable/ic_arrow_right.xml b/feature/stamp/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 00000000..f6f940a4 --- /dev/null +++ b/feature/stamp/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,12 @@ + + + diff --git a/feature/stamp/src/main/res/drawable/ic_checked_stamp.png b/feature/stamp/src/main/res/drawable/ic_checked_stamp.png new file mode 100644 index 00000000..78fd9b1d Binary files /dev/null and b/feature/stamp/src/main/res/drawable/ic_checked_stamp.png differ diff --git a/feature/stamp/src/main/res/drawable/ic_refresh.xml b/feature/stamp/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 00000000..5e1884e9 --- /dev/null +++ b/feature/stamp/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/stamp/src/main/res/drawable/ic_unchecked_stamp.png b/feature/stamp/src/main/res/drawable/ic_unchecked_stamp.png new file mode 100644 index 00000000..1f90ca78 Binary files /dev/null and b/feature/stamp/src/main/res/drawable/ic_unchecked_stamp.png differ diff --git a/feature/stamp/src/main/res/values/strings.xml b/feature/stamp/src/main/res/values/strings.xml new file mode 100644 index 00000000..75fe65b0 --- /dev/null +++ b/feature/stamp/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + + 스탬프 + 스탬프 받기 + 지금까지 모은 스탬프 + 새로고침 + 스탬프 부스 찾아보기 + 스탬프 QR 스캔 + 카메라를 조준선에 맞춰주세요! + ※ 하나의 부스에서는 하나의 스탬프만 받을 수 있습니다. + 스탬프 가능 부스 + + diff --git a/feature/waiting/build.gradle.kts b/feature/waiting/build.gradle.kts index e6c8d31f..b716a924 100644 --- a/feature/waiting/build.gradle.kts +++ b/feature/waiting/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.unifest.android.feature) + alias(libs.plugins.kotlin.serialization) // alias(libs.plugins.compose.investigator) } @@ -11,6 +12,8 @@ android { dependencies { implementations( + projects.core.data, + libs.kotlinx.collections.immutable, libs.timber, ) diff --git a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/WaitingScreen.kt b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/WaitingScreen.kt index 614e807e..a5d0f3e6 100644 --- a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/WaitingScreen.kt +++ b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/WaitingScreen.kt @@ -1,41 +1,237 @@ package com.unifest.android.feature.waiting +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardColors +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshContainer +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.unifest.android.core.common.ObserveAsEvents +import com.unifest.android.core.designsystem.theme.BoothTitle2 +import com.unifest.android.core.designsystem.theme.Content2 +import com.unifest.android.core.designsystem.theme.Content7 +import com.unifest.android.core.designsystem.theme.Title4 import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.designsystem.theme.WaitingNumber4 import com.unifest.android.core.ui.DevicePreview +import com.unifest.android.core.ui.component.NoShowWaitingCancelDialog +import com.unifest.android.core.ui.component.WaitingCancelDialog +import com.unifest.android.feature.waiting.component.WaitingInfoItem +import com.unifest.android.feature.waiting.preview.WaitingPreviewParameterProvider +import com.unifest.android.feature.waiting.viewmodel.WaitingUiAction +import com.unifest.android.feature.waiting.viewmodel.WaitingUiEvent +import com.unifest.android.feature.waiting.viewmodel.WaitingUiState +import com.unifest.android.feature.waiting.viewmodel.WaitingViewModel +import kotlinx.coroutines.delay @Composable -internal fun WaitingRoute(padding: PaddingValues) { - WaitingScreen(padding = padding) +internal fun WaitingRoute( + padding: PaddingValues, + popBackStack: () -> Unit, + navigateToBoothDetail: (Long) -> Unit, + viewModel: WaitingViewModel = hiltViewModel(), +) { + val waitingUiState by viewModel.uiState.collectAsStateWithLifecycle() + + ObserveAsEvents(flow = viewModel.uiEvent) { event -> + when (event) { + is WaitingUiEvent.NavigateBack -> popBackStack() + is WaitingUiEvent.NavigateToMap -> popBackStack() + is WaitingUiEvent.NavigateToBoothDetail -> navigateToBoothDetail(event.boothId) + } + } + LaunchedEffect(key1 = Unit) { + viewModel.getMyWaitingList() + } + + WaitingScreen( + padding = padding, + waitingUiState = waitingUiState, + onWaitingUiAction = viewModel::onWaitingUiAction, + ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun WaitingScreen( padding: PaddingValues, + waitingUiState: WaitingUiState, + onWaitingUiAction: (WaitingUiAction) -> Unit, ) { - Column( + val pullToRefreshState = rememberPullToRefreshState() + + LaunchedEffect(key1 = pullToRefreshState.isRefreshing) { + if (pullToRefreshState.isRefreshing) { + delay(1000) + onWaitingUiAction(WaitingUiAction.OnRefresh) + pullToRefreshState.endRefresh() + } + } + + Box( modifier = Modifier .fillMaxSize() + .nestedScroll(pullToRefreshState.nestedScrollConnection) + .background(MaterialTheme.colorScheme.background) .padding(padding), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, ) { - Text("Waiting Screen") + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(20.dp), + ) { + item { + Text( + text = stringResource(id = R.string.waiting_title), + color = MaterialTheme.colorScheme.onBackground, + style = BoothTitle2, + ) + } + item { Spacer(modifier = Modifier.height(16.dp)) } + item { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(29.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), + colors = CardColors( + containerColor = MaterialTheme.colorScheme.tertiary, + contentColor = MaterialTheme.colorScheme.primary, + disabledContainerColor = MaterialTheme.colorScheme.secondaryContainer, + disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(id = R.string.waiting_my_waiting), + style = Title4, + color = MaterialTheme.colorScheme.primary, + ) + } + } + } + item { Spacer(modifier = Modifier.height(10.dp)) } + item { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(id = R.string.waiting_total_cases, waitingUiState.myWaitingList.size), + color = MaterialTheme.colorScheme.onBackground, + style = Content7, + ) + } + } + item { Spacer(modifier = Modifier.height(8.dp)) } + itemsIndexed( + items = waitingUiState.myWaitingList, + key = { _, waitingItem -> waitingItem.waitingId }, + ) { _, waitingItem -> + Column { + Spacer(modifier = Modifier.height(16.dp)) + WaitingInfoItem( + myWaitingModel = waitingItem, + onWaitingUiAction = onWaitingUiAction, + ) + } + Spacer(modifier = Modifier.height(8.dp)) + } + } + if (pullToRefreshState.isRefreshing) { + PullToRefreshContainer( + modifier = Modifier.align(Alignment.TopCenter), + state = pullToRefreshState, + ) + } + } + if (waitingUiState.myWaitingList.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(id = R.string.waiting_no_waiting), + style = WaitingNumber4, + color = MaterialTheme.colorScheme.onBackground, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.waiting_no_waiting_description), + style = Content2.copy( + textDecoration = TextDecoration.Underline, + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.clickable { + onWaitingUiAction(WaitingUiAction.OnLookForBoothClick) + }, + ) + } + } + } + + if (waitingUiState.isWaitingCancelDialogVisible) { + WaitingCancelDialog( + onCancelClick = { onWaitingUiAction(WaitingUiAction.OnWaitingCancelDialogCancelClick) }, + onConfirmClick = { onWaitingUiAction(WaitingUiAction.OnWaitingCancelDialogConfirmClick) }, + ) + } + + if (waitingUiState.isNoShowWaitingCancelDialogVisible) { + NoShowWaitingCancelDialog( + onCancelClick = { onWaitingUiAction(WaitingUiAction.OnNoShowWaitingCancelDialogCancelClick) }, + onConfirmClick = { onWaitingUiAction(WaitingUiAction.OnNoShowWaitingCancelDialogConfirmClick) }, + ) } } @DevicePreview @Composable -fun WaitingScreenPreview() { +fun WaitingScreenPreview( + @PreviewParameter(WaitingPreviewParameterProvider::class) + waitingUiState: WaitingUiState, +) { UnifestTheme { - WaitingScreen(padding = PaddingValues()) + WaitingScreen( + padding = PaddingValues(), + waitingUiState = waitingUiState, + onWaitingUiAction = {}, + ) } } diff --git a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/component/WaitingInfoItem.kt b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/component/WaitingInfoItem.kt new file mode 100644 index 00000000..fc959092 --- /dev/null +++ b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/component/WaitingInfoItem.kt @@ -0,0 +1,248 @@ +package com.unifest.android.feature.waiting.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.unifest.android.core.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.component.UnifestButton +import com.unifest.android.core.designsystem.theme.Content1 +import com.unifest.android.core.designsystem.theme.LightPrimary700 +import com.unifest.android.core.designsystem.theme.Title3 +import com.unifest.android.core.designsystem.theme.Title5 +import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.designsystem.theme.WaitingNumber +import com.unifest.android.core.designsystem.theme.WaitingNumber2 +import com.unifest.android.core.designsystem.theme.WaitingNumber5 +import com.unifest.android.core.model.MyWaitingModel +import com.unifest.android.feature.waiting.R +import com.unifest.android.feature.waiting.viewmodel.WaitingUiAction +import com.unifest.android.core.designsystem.R as designR + +@Composable +fun WaitingInfoItem( + myWaitingModel: MyWaitingModel, + onWaitingUiAction: (WaitingUiAction) -> Unit, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .shadow(elevation = 8.dp, shape = RoundedCornerShape(8.dp)) + .background( + color = if (myWaitingModel.status != "NOSHOW") { + MaterialTheme.colorScheme.surfaceBright + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + }, + shape = RoundedCornerShape(8.dp), + ), + ) { + Column( + modifier = Modifier + .then( + if (myWaitingModel.status != "NOSHOW") { + Modifier.background(MaterialTheme.colorScheme.surfaceBright) + } else { + Modifier + }, + ) + .padding(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringResource(id = R.string.waiting_current_order), + color = MaterialTheme.colorScheme.onBackground, + style = Content1, + ) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = designR.drawable.ic_location_green), + contentDescription = "Location Icon", + modifier = Modifier.align(Alignment.CenterVertically), + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = myWaitingModel.boothName, + color = MaterialTheme.colorScheme.onBackground, + style = Title3, + ) + } + } + Spacer(modifier = Modifier.height(2.dp)) + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Row( + verticalAlignment = Alignment.Bottom, + ) { + Text( + text = when (myWaitingModel.status) { + "NOSHOW" -> stringResource(id = R.string.waiting_no_show) + "CALLED" -> stringResource(id = R.string.waiting_my_turn) + else -> myWaitingModel.waitingOrder.toString() + }, + style = if (myWaitingModel.status == "CALLED") WaitingNumber5 else WaitingNumber, + color = if (myWaitingModel.status == "NOSHOW") { + LightPrimary700 + } else { + MaterialTheme.colorScheme.primary + }, + modifier = Modifier.alignByBaseline(), + ) + Spacer(modifier = Modifier.width(8.dp)) + if (myWaitingModel.status != "CALLED" && myWaitingModel.status != "NOSHOW") { + Text( + text = stringResource(id = R.string.waiting_nth), + fontSize = 18.sp, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.alignByBaseline(), + ) + } + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.waiting_waiting_number), + color = MaterialTheme.colorScheme.onBackground, + style = Content1, + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = myWaitingModel.waitingId.toString(), + color = MaterialTheme.colorScheme.onBackground, + style = WaitingNumber2, + ) + Spacer(modifier = Modifier.width(8.dp)) + VerticalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.height(13.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(id = R.string.waiting_people), + color = MaterialTheme.colorScheme.onBackground, + style = Content1, + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = myWaitingModel.partySize.toString(), + color = MaterialTheme.colorScheme.onBackground, + style = WaitingNumber2, + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + UnifestButton( + onClick = { + if (myWaitingModel.status == "NOSHOW") { + onWaitingUiAction(WaitingUiAction.OnCancelWaitingClick(myWaitingModel.waitingId)) + } else { + onWaitingUiAction(WaitingUiAction.OnCancelWaitingClick(myWaitingModel.waitingId)) + } + }, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier.weight(1f), + ) { + Text( + text = if (myWaitingModel.status == "NOSHOW") { + stringResource(id = R.string.waiting_no_show_description) + } else { + stringResource(id = R.string.waiting_cancel_waiting) + }, + color = MaterialTheme.colorScheme.surfaceTint, + style = Title5, + ) + } + Spacer(modifier = Modifier.width(10.dp)) + UnifestButton( + onClick = { onWaitingUiAction(WaitingUiAction.OnCheckBoothDetailClick(myWaitingModel.boothId)) }, + containerColor = MaterialTheme.colorScheme.surfaceContainer, + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(id = R.string.waiting_booth_check), + color = MaterialTheme.colorScheme.onSurface, + style = Title5, + ) + } + } + } + } +} + +@ComponentPreview +@Composable +fun WaitingInfoItemPreview() { + UnifestTheme { + WaitingInfoItem( + myWaitingModel = MyWaitingModel( + boothId = 1L, + waitingId = 1L, + partySize = 2L, + tel = "010-1234-5678", + deviceId = "1234567890", + createdAt = "2024-05-23", + updatedAt = "2024-05-23", + status = "waiting", + waitingOrder = 1L, + boothName = "부스 이름", + ), + onWaitingUiAction = {}, + ) + } +} + +@ComponentPreview +@Composable +fun WaitingInfoItemPreviewNOSHOW() { + UnifestTheme { + WaitingInfoItem( + myWaitingModel = MyWaitingModel( + boothId = 1L, + waitingId = 1L, + partySize = 2L, + tel = "010-1234-5678", + deviceId = "1234567890", + createdAt = "2024-05-23", + updatedAt = "2024-05-23", + status = "NOSHOW", + waitingOrder = 1L, + boothName = "부스 이름", + ), + onWaitingUiAction = {}, + ) + } +} diff --git a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/navigation/WaitingNavigation.kt b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/navigation/WaitingNavigation.kt index 10f5c808..9acbb511 100644 --- a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/navigation/WaitingNavigation.kt +++ b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/navigation/WaitingNavigation.kt @@ -5,18 +5,23 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable +import com.unifest.android.core.navigation.MainTabRoute import com.unifest.android.feature.waiting.WaitingRoute -const val WAITING_ROUTE = "waiting_route" - fun NavController.navigateToWaiting(navOptions: NavOptions) { - navigate(WAITING_ROUTE, navOptions) + navigate(MainTabRoute.Waiting, navOptions) } fun NavGraphBuilder.waitingNavGraph( padding: PaddingValues, + popBackStack: () -> Unit, + navigateToBoothDetail: (Long) -> Unit, ) { - composable(route = WAITING_ROUTE) { - WaitingRoute(padding = padding) + composable { + WaitingRoute( + padding = padding, + popBackStack = popBackStack, + navigateToBoothDetail = navigateToBoothDetail, + ) } } diff --git a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/preview/WaitingPreviewParameterProvider.kt b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/preview/WaitingPreviewParameterProvider.kt new file mode 100644 index 00000000..be08ccfa --- /dev/null +++ b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/preview/WaitingPreviewParameterProvider.kt @@ -0,0 +1,50 @@ +package com.unifest.android.feature.waiting.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.unifest.android.core.model.MyWaitingModel +import com.unifest.android.feature.waiting.viewmodel.WaitingUiState +import kotlinx.collections.immutable.persistentListOf + +internal class WaitingPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + WaitingUiState( + myWaitingList = persistentListOf( + MyWaitingModel( + boothId = 1L, + waitingId = 1L, + partySize = 4, + tel = "010-1234-5678", + waitingOrder = 1, + ), + MyWaitingModel( + boothId = 2L, + waitingId = 2L, + partySize = 4, + tel = "010-1234-5678", + waitingOrder = 2, + ), + MyWaitingModel( + boothId = 3L, + waitingId = 3L, + partySize = 4, + tel = "010-1234-5678", + waitingOrder = 3, + ), + MyWaitingModel( + boothId = 4L, + waitingId = 4L, + partySize = 4, + tel = "010-1234-5678", + waitingOrder = 4, + ), + MyWaitingModel( + boothId = 5L, + waitingId = 5L, + partySize = 4, + tel = "010-1234-5678", + waitingOrder = 5, + ), + ), + ), + ) +} diff --git a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingUiAction.kt b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingUiAction.kt new file mode 100644 index 00000000..468bf129 --- /dev/null +++ b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingUiAction.kt @@ -0,0 +1,14 @@ +package com.unifest.android.feature.waiting.viewmodel + +sealed interface WaitingUiAction { + data class OnCancelWaitingClick(val waitingId: Long) : WaitingUiAction + data class OnCancelNoShowWaitingClick(val waitingId: Long) : WaitingUiAction + data class OnCheckBoothDetailClick(val boothId: Long) : WaitingUiAction + data object OnPullToRefresh : WaitingUiAction + data object OnWaitingCancelDialogCancelClick : WaitingUiAction + data object OnWaitingCancelDialogConfirmClick : WaitingUiAction + data object OnNoShowWaitingCancelDialogCancelClick : WaitingUiAction + data object OnNoShowWaitingCancelDialogConfirmClick : WaitingUiAction + data object OnLookForBoothClick : WaitingUiAction + data object OnRefresh : WaitingUiAction +} diff --git a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingUiEvent.kt b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingUiEvent.kt new file mode 100644 index 00000000..8e390ced --- /dev/null +++ b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingUiEvent.kt @@ -0,0 +1,7 @@ +package com.unifest.android.feature.waiting.viewmodel + +sealed interface WaitingUiEvent { + data object NavigateBack : WaitingUiEvent + data object NavigateToMap : WaitingUiEvent + data class NavigateToBoothDetail(val boothId: Long) : WaitingUiEvent +} diff --git a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingUiState.kt b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingUiState.kt new file mode 100644 index 00000000..fe58652a --- /dev/null +++ b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingUiState.kt @@ -0,0 +1,15 @@ +package com.unifest.android.feature.waiting.viewmodel + +import com.unifest.android.core.model.MyWaitingModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class WaitingUiState( + val isLoading: Boolean = false, + val myWaitingList: ImmutableList = persistentListOf(), + val isServerErrorDialogVisible: Boolean = false, + val isNetworkErrorDialogVisible: Boolean = false, + val isWaitingCancelDialogVisible: Boolean = false, + val isNoShowWaitingCancelDialogVisible: Boolean = false, + val waitingCancelDialogWaitingId: Long = 0, +) diff --git a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingViewModel.kt b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingViewModel.kt index ce23aecb..b34aee85 100644 --- a/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingViewModel.kt +++ b/feature/waiting/src/main/kotlin/com/unifest/android/feature/waiting/viewmodel/WaitingViewModel.kt @@ -1,8 +1,125 @@ package com.unifest.android.feature.waiting.viewmodel import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.unifest.android.core.common.ErrorHandlerActions +import com.unifest.android.core.common.handleException +import com.unifest.android.core.data.repository.WaitingRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class WaitingViewModel @Inject constructor() : ViewModel() +class WaitingViewModel @Inject constructor( + private val waitingRepository: WaitingRepository, +) : ViewModel(), ErrorHandlerActions { + private val _uiState = MutableStateFlow(WaitingUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + private val _uiEvent = Channel() + val uiEvent: Flow = _uiEvent.receiveAsFlow() + + init { + getMyWaitingList() + } + + fun onWaitingUiAction(action: WaitingUiAction) { + when (action) { + is WaitingUiAction.OnCancelWaitingClick -> setWaitingCancelDialogWaitingId(action.waitingId) + is WaitingUiAction.OnCancelNoShowWaitingClick -> setNoShowWaitingCancelDialogWaitingId(action.waitingId) + is WaitingUiAction.OnCheckBoothDetailClick -> navigateToBoothDetail(action.boothId) + is WaitingUiAction.OnPullToRefresh -> setNetworkErrorDialogVisible(false) + is WaitingUiAction.OnWaitingCancelDialogCancelClick -> setWaitingCancelDialogVisible(false) + is WaitingUiAction.OnWaitingCancelDialogConfirmClick -> cancelBoothWaiting() + is WaitingUiAction.OnNoShowWaitingCancelDialogCancelClick -> setNoShowWaitingCancelDialogVisible(false) + is WaitingUiAction.OnNoShowWaitingCancelDialogConfirmClick -> cancelBoothWaiting() + is WaitingUiAction.OnLookForBoothClick -> navigateToMap() + is WaitingUiAction.OnRefresh -> getMyWaitingList() + } + } + + fun getMyWaitingList() { + viewModelScope.launch { + waitingRepository.getMyWaitingList() + .onSuccess { waitingLists -> + _uiState.update { + it.copy(myWaitingList = waitingLists.toImmutableList()) + } + } + .onFailure { exception -> + handleException(exception, this@WaitingViewModel) + } + } + } + + private fun setWaitingCancelDialogWaitingId(waitingId: Long) { + setWaitingCancelDialogVisible(true) + _uiState.update { + it.copy(waitingCancelDialogWaitingId = waitingId) + } + } + + private fun setNoShowWaitingCancelDialogWaitingId(waitingId: Long) { + setNoShowWaitingCancelDialogVisible(true) + _uiState.update { + it.copy(waitingCancelDialogWaitingId = waitingId) + } + } + + private fun cancelBoothWaiting() { + viewModelScope.launch { + waitingRepository.cancelBoothWaiting(_uiState.value.waitingCancelDialogWaitingId) + .onSuccess { + getMyWaitingList() + setWaitingCancelDialogVisible(false) + } + .onFailure { exception -> + setWaitingCancelDialogVisible(false) + handleException(exception, this@WaitingViewModel) + } + } + } + + private fun navigateToBoothDetail(boothId: Long) { + viewModelScope.launch { + _uiEvent.send(WaitingUiEvent.NavigateToBoothDetail(boothId)) + } + } + + private fun navigateToMap() { + viewModelScope.launch { + _uiEvent.send(WaitingUiEvent.NavigateToMap) + } + } + + private fun setWaitingCancelDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isWaitingCancelDialogVisible = flag) + } + } + + private fun setNoShowWaitingCancelDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isNoShowWaitingCancelDialogVisible = flag) + } + } + + override fun setServerErrorDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isServerErrorDialogVisible = flag) + } + } + + override fun setNetworkErrorDialogVisible(flag: Boolean) { + _uiState.update { + it.copy(isNetworkErrorDialogVisible = flag) + } + } +} diff --git a/feature/waiting/src/main/res/values/strings.xml b/feature/waiting/src/main/res/values/strings.xml new file mode 100644 index 00000000..c9a3fc3a --- /dev/null +++ b/feature/waiting/src/main/res/values/strings.xml @@ -0,0 +1,19 @@ + + + 웨이팅 + 나의 웨이팅 + 총 %d건 + 정렬 + 현재 내 순서 + 웨이팅번호 + 인원 + 웨이팅 취소 + 부스 확인하기 + 번째 + 신청한 웨이팅이 없어요 + 주점/부스 구경하러 가기> + 입장해주세요 + 부재 처리 + 부재 웨이팅 지우기 + + diff --git a/gradle.properties b/gradle.properties index 0b3156b2..90199340 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,3 +24,5 @@ android.nonTransitiveRClass=true android.enableJetifier=true UNIFEST_CONTACT_URL = "http://pf.kakao.com/_KxaaDG/chat" UNIFEST_WEB_URL = "https://www.unifest.app" +UNIFEST_PRIVATE_POLICY_URL = "https://beaded-alley-5ed.notion.site/0398cc021c9d4879bdfbcd031d56da5e?pvs=4" +UNIFEST_THIRD_PARTY_POLICY_URL = "https://beaded-alley-5ed.notion.site/3-f1a3be0abb3840799b1131b1c7b5d2ca?pvs=4" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f97bd6a2..f15330e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,75 +4,68 @@ minSdk = "26" targetSdk = "34" compileSdk = "34" versionCode = "17" -versionName = "1.0.7" +versionName = "1.1.0" packageName = "com.unifest.android" -android-gradle-plugin = "8.2.2" -kotlin-core = "1.9.23" +android-gradle-plugin = "8.5.2" +kotlin-core = "2.0.20" gradle-dependency-handler-extensions = "1.1.0" google-secrets = "2.0.1" -google-service = "4.4.1" +google-service = "4.4.2" kotlin-detekt = "1.23.6" kotlin-ktlint-gradle = "11.6.1" kotlin-ktlint-source = "0.50.0" -kotlinx-coroutines = "1.8.0" -kotlinx-datetime = "0.5.0" -kotlinx-serialization = "1.6.3" +kotlinx-coroutines = "1.9.0" +kotlinx-datetime = "0.6.1" +kotlinx-serialization = "1.7.2" kotlinx-serialization-converter = "1.0.0" -kotlinx-collections-immutable = "0.3.7" +kotlinx-collections-immutable = "0.3.8" -android-play-services-location = "21.0.1" +android-play-services-location = "21.3.0" androidx-core = "1.13.1" -androidx-lifecycle = "2.8.0-alpha02" +androidx-lifecycle = "2.8.6" androidx-splash = "1.0.1" -androidx-startup = "1.1.1" -androidx-navigation = "2.7.7" +androidx-startup = "1.2.0" +androidx-navigation = "2.8.1" androidx-datastore = "1.1.1" androidx-room = "2.6.1" -androidx-activity-compose = "1.9.0" -androidx-compose-compiler = "1.5.11" -androidx-compose-bom = "2024.05.00" +androidx-activity-compose = "1.9.2" +androidx-compose = "1.6.8" androidx-compose-material3 = "1.2.1" -androidx-compose-animation = "1.7.0-alpha08" androidx-hilt-navigation-compose = "1.2.0" -desugar-jdk-libs = "2.0.4" -hilt = "2.51.1" +desugar-jdk-libs = "2.1.2" +hilt = "2.52" javax-inject = "1" retrofit = "2.11.0" okhttp = "5.0.0-alpha.14" -coil-compose = "2.6.0" +coil-compose = "2.7.0" timber = "5.0.1" -firebase-bom = "32.8.1" -firebase-crashlytics = "2.9.9" -firebase-config = "21.6.3" -ksp = "1.9.23-1.0.20" -compose-extensions = "1.6.5" -compose-stable-marker = "1.0.4" +firebase-bom = "33.3.0" +firebase-crashlytics = "3.0.2" +ksp = "2.0.20-1.0.25" +compose-extensions = "1.6.8" +compose-stable-marker = "1.0.5" compose-investigator = "1.5.11-0.2.1" -landscapist = "2.3.3" -balloon = "1.6.4" -calendar-compose = "2.5.0" -flexible-bottomsheet = "0.1.2" - -naver-map-compose = "1.5.7" +landscapist = "2.3.6" +balloon = "1.6.7" +calendar-compose = "2.6.0-beta04" +flexible-bottomsheet = "0.1.5" +naver-map-compose = "1.7.1" naver-map-location = "21.0.1" tedclustering-naver = "1.0.2" -junit = "4.13.2" -junitVersion = "1.1.5" -espressoCore = "3.5.1" -appcompat = "1.6.1" -material = "1.12.0" +zxing = "4.3.0" [libraries] gradle-android = { group = "com.android.tools.build", name = "gradle", version.ref = "android-gradle-plugin" } gradle-kotlin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin-core" } gradle-androidx-room = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "androidx-room" } +compose-compiler-extension = { group = "org.jetbrains.kotlin", name = "compose-compiler-gradle-plugin", version.ref = "kotlin-core" } kotlin-ktlint = { group = "com.pinterest", name = "ktlint", version.ref = "kotlin-ktlint-source" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } @@ -93,15 +86,13 @@ androidx-activity-compose = { group = "androidx.activity", name = "activity-comp androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" } -androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } -androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidx-compose" } +androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidx-compose" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } -androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class" } -androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } -androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } -androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "androidx-compose" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidx-compose" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidx-compose" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidx-hilt-navigation-compose" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" } @@ -123,6 +114,7 @@ firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.r firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" } firebase-config = { group = "com.google.firebase", name = "firebase-config-ktx" } +firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx"} coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil-compose" } compose-stable-marker = { group = "com.github.skydoves", name = "compose-stable-marker", version.ref = "compose-stable-marker" } landscapist-bom = { group = "com.github.skydoves", name = "landscapist-bom", version.ref = "landscapist" } @@ -131,15 +123,10 @@ landscapist-placeholder = { group = "com.github.skydoves", name = "landscapist-p ballon-compose = { group = "com.github.skydoves", name = "balloon-compose", version.ref = "balloon" } calendar-compose = { group = "com.kizitonwose.calendar", name = "compose", version.ref = "calendar-compose" } flexible-bottomsheet = { group = "com.github.skydoves", name = "flexible-bottomsheet-material3", version.ref = "flexible-bottomsheet" } - naver-map-compose = { group = "io.github.fornewid", name = "naver-map-compose", version.ref = "naver-map-compose" } naver-map-location = { group = "io.github.fornewid", name = "naver-map-location", version.ref = "naver-map-location" } tedclustering-naver = { group = "io.github.ParkSangGwon", name = "tedclustering-naver", version.ref = "tedclustering-naver" } -junit = { group = "junit", name = "junit", version.ref = "junit" } -androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } -androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } +zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing" } [plugins] @@ -157,6 +144,7 @@ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-core" } android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin-core" } androidx-room = { id = "androidx.room", version.ref = "androidx-room" } @@ -179,6 +167,16 @@ unifest-jvm-kotlin = { id = "unifest.jvm.kotlin", version = "unspecified" } [bundles] +androidx-compose = [ + "androidx-compose-foundation", + "androidx-compose-material-iconsExtended", + "androidx-compose-material3", + "androidx-compose-runtime", + "androidx-compose-ui", + "androidx-compose-ui-tooling", + "androidx-compose-ui-tooling-preview", +] + androidx-lifecycle = [ "androidx-lifecycle-runtime-compose", "androidx-lifecycle-viewmodel-compose", diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136..a4b76b95 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a..9355b415 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30db..9b42019c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,92 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts index d0675e60..0162876b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,6 +31,7 @@ include( ":core:datastore", ":core:designsystem", ":core:model", + ":core:navigation", ":core:network", ":core:ui", @@ -45,4 +46,5 @@ include( ":feature:navigator", ":feature:splash", ":feature:waiting", + ":feature:stamp", ) diff --git a/stability.config.conf b/stability.config.conf new file mode 100644 index 00000000..61e965a3 --- /dev/null +++ b/stability.config.conf @@ -0,0 +1 @@ +java.time.LocalDate