diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 2e8d91b6..a1131e9c 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -16,8 +16,7 @@ gradlePlugin { "android.feature" to "AndroidFeatureConventionPlugin", "android.hilt" to "AndroidHiltConventionPlugin", "android.retrofit" to "AndroidRetrofitConventionPlugin", - "jvm.kotlin" to "JvmKotlinConventionPlugin", - "test.kotest" to "TestKotestConventionPlugin", + "android.room" to "AndroidRoomConventionPlugin", ) plugins { @@ -45,6 +44,7 @@ kotlin { dependencies { compileOnly(libs.gradle.android) compileOnly(libs.gradle.kotlin) + compileOnly(libs.gradle.androidx.room) compileOnly(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } diff --git a/build-logic/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidRoomConventionPlugin.kt new file mode 100644 index 00000000..5a2eb0e8 --- /dev/null +++ b/build-logic/src/main/kotlin/AndroidRoomConventionPlugin.kt @@ -0,0 +1,28 @@ +import androidx.room.gradle.RoomExtension +import com.unifest.android.Plugins +import com.unifest.android.applyPlugins +import com.unifest.android.implementation +import com.unifest.android.ksp +import com.unifest.android.libs +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies + +class AndroidRoomConventionPlugin : BuildLogicConventionPlugin( + { + applyPlugins(Plugins.AndroidxRoom, Plugins.KotlinxSerialization, Plugins.Ksp) + + extensions.configure { + // The schemas directory contains a schema file for each version of the Room database. + // This is required to enable Room auto migrations. + // See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration. + schemaDirectory("$projectDir/schemas") + } + + dependencies { + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) + implementation(libs.kotlinx.serialization.json) + } + }, +) 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 ca86ca4a..89ef4d09 100644 --- a/build-logic/src/main/kotlin/com/unifest/android/Plugins.kt +++ b/build-logic/src/main/kotlin/com/unifest/android/Plugins.kt @@ -1,9 +1,6 @@ package com.unifest.android internal object Plugins { - const val JavaLibrary = "java-library" - - const val KotlinJvm = "org.jetbrains.kotlin.jvm" const val KotlinAndroid = "org.jetbrains.kotlin.android" const val KotlinxSerialization = "org.jetbrains.kotlin.plugin.serialization" @@ -11,6 +8,8 @@ internal object Plugins { const val AndroidApplication = "com.android.application" const val AndroidLibrary = "com.android.library" + const val AndroidxRoom = "androidx.room" + const val hilt = "dagger.hilt.android.plugin" const val Ksp = "com.google.devtools.ksp" diff --git a/build.gradle.kts b/build.gradle.kts index b00b8874..ae73f2e4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,6 +9,7 @@ plugins { alias(libs.plugins.kotlin.serialization) 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.hilt) apply false alias(libs.plugins.google.service) apply false alias(libs.plugins.firebase.crashlytics) apply false diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index b0da1463..eea97634 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementations( projects.core.network, projects.core.datastore, + projects.core.database, libs.timber, ) 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 new file mode 100644 index 00000000..05bdbaa9 --- /dev/null +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/di/RepositoryModule.kt @@ -0,0 +1,18 @@ +package com.unifest.android.core.data.di + +import com.unifest.android.core.data.repository.DefaultLikedBoothRepository +import com.unifest.android.core.data.repository.LikedBoothRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + + @Binds + @Singleton + abstract fun bindLikedBoothRepository(likedBoothRepository: DefaultLikedBoothRepository): LikedBoothRepository +} diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/DBEntityToResponse.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/DBEntityToResponse.kt new file mode 100644 index 00000000..09e37ef3 --- /dev/null +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/DBEntityToResponse.kt @@ -0,0 +1,29 @@ +package com.unifest.android.core.data.mapper + +import com.unifest.android.core.data.response.BoothDetailResponse +import com.unifest.android.core.data.response.MenuResponse +import com.unifest.android.core.database.entity.LikedBoothEntity +import com.unifest.android.core.database.entity.MenuEntity + +internal fun LikedBoothEntity.toResponse(): BoothDetailResponse { + return BoothDetailResponse( + id = id, + name = name, + category = category, + description = description, + warning = warning, + location = location, + latitude = latitude, + longitude = longitude, + menus = menus.map { it.toResponse() }, + ) +} + +internal fun MenuEntity.toResponse(): MenuResponse { + return MenuResponse( + id = id, + name = name, + price = price, + imgUrl = imgUrl, + ) +} diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/ResponseToDBEntity.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/ResponseToDBEntity.kt new file mode 100644 index 00000000..c6c6bf5e --- /dev/null +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/ResponseToDBEntity.kt @@ -0,0 +1,30 @@ +package com.unifest.android.core.data.mapper + +import com.unifest.android.core.data.response.BoothDetailResponse +import com.unifest.android.core.data.response.MenuResponse +import com.unifest.android.core.database.entity.LikedBoothEntity +import com.unifest.android.core.database.entity.MenuEntity + +internal fun BoothDetailResponse.toDBEntity(): LikedBoothEntity { + return LikedBoothEntity( + id = id, + name = name, + category = category, + description = description, + warning = warning, + location = location, + latitude = latitude, + longitude = longitude, + menus = menus.map { it.toDBEntity() }, + isLiked = false, + ) +} + +internal fun MenuResponse.toDBEntity(): MenuEntity { + return MenuEntity( + id = id, + name = name, + price = price, + imgUrl = imgUrl, + ) +} diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/ResponseToEntity.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/ResponseToEntity.kt deleted file mode 100644 index dd7af9c6..00000000 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/mapper/ResponseToEntity.kt +++ /dev/null @@ -1 +0,0 @@ -package com.unifest.android.core.data.mapper diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/DefaultLikedBoothRepository.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/DefaultLikedBoothRepository.kt new file mode 100644 index 00000000..0a25d9b4 --- /dev/null +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/DefaultLikedBoothRepository.kt @@ -0,0 +1,33 @@ +package com.unifest.android.core.data.repository + +import com.unifest.android.core.data.mapper.toDBEntity +import com.unifest.android.core.data.mapper.toResponse +import com.unifest.android.core.data.response.BoothDetailResponse +import com.unifest.android.core.database.LikedBoothDao +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class DefaultLikedBoothRepository @Inject constructor( + private val likedBoothDao: LikedBoothDao, +) : LikedBoothRepository { + override fun getLikedBoothList(): Flow> { + return likedBoothDao.getLikedBoothList().map { likedBooths -> + likedBooths.map { likedBooth -> + likedBooth.toResponse() + } + } + } + + override suspend fun insertLikedBooth(booth: BoothDetailResponse) { + likedBoothDao.insertLikedBooth(booth.toDBEntity()) + } + + override suspend fun deleteLikedBooth(booth: BoothDetailResponse) { + likedBoothDao.deleteLikedBooth(booth.toDBEntity()) + } + + override suspend fun updateLikedBooth(booth: BoothDetailResponse) { + likedBoothDao.updateLikedBooth(booth.id, booth.isLiked) + } +} diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedBoothRepository.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedBoothRepository.kt new file mode 100644 index 00000000..664edba3 --- /dev/null +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/repository/LikedBoothRepository.kt @@ -0,0 +1,11 @@ +package com.unifest.android.core.data.repository + +import com.unifest.android.core.data.response.BoothDetailResponse +import kotlinx.coroutines.flow.Flow + +interface LikedBoothRepository { + fun getLikedBoothList(): Flow> + suspend fun insertLikedBooth(booth: BoothDetailResponse) + suspend fun deleteLikedBooth(booth: BoothDetailResponse) + suspend fun updateLikedBooth(booth: BoothDetailResponse) +} diff --git a/core/data/src/main/kotlin/com/unifest/android/core/data/response/BoothDetailResponse.kt b/core/data/src/main/kotlin/com/unifest/android/core/data/response/BoothDetailResponse.kt index 0ba561da..cbcfba40 100644 --- a/core/data/src/main/kotlin/com/unifest/android/core/data/response/BoothDetailResponse.kt +++ b/core/data/src/main/kotlin/com/unifest/android/core/data/response/BoothDetailResponse.kt @@ -23,6 +23,7 @@ data class BoothDetailResponse( val longitude: Float, @SerialName("menus") val menus: List, + val isLiked: Boolean = false, ) @Serializable diff --git a/feature/interested-booth/.gitignore b/core/database/.gitignore similarity index 100% rename from feature/interested-booth/.gitignore rename to core/database/.gitignore diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts new file mode 100644 index 00000000..044ceec9 --- /dev/null +++ b/core/database/build.gradle.kts @@ -0,0 +1,18 @@ +@file:Suppress("INLINE_FROM_HIGHER_PLATFORM") + +plugins { + alias(libs.plugins.unifest.android.library) + alias(libs.plugins.unifest.android.hilt) + alias(libs.plugins.unifest.android.room) + id("kotlinx-serialization") +} + +android { + namespace = "com.unifest.android.core.database" +} + +dependencies { + implementations( + libs.timber, + ) +} diff --git a/core/database/src/main/kotlin/com/unifest/android/core/database/LikedBoothDao.kt b/core/database/src/main/kotlin/com/unifest/android/core/database/LikedBoothDao.kt new file mode 100644 index 00000000..712754e6 --- /dev/null +++ b/core/database/src/main/kotlin/com/unifest/android/core/database/LikedBoothDao.kt @@ -0,0 +1,24 @@ +package com.unifest.android.core.database + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.unifest.android.core.database.entity.LikedBoothEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface LikedBoothDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertLikedBooth(userInfo: LikedBoothEntity) + + @Delete + suspend fun deleteLikedBooth(userInfo: LikedBoothEntity) + + @Query("SELECT * FROM liked_booth") + fun getLikedBoothList(): Flow> + + @Query("UPDATE liked_booth SET is_liked = :isLiked WHERE id = :id") + suspend fun updateLikedBooth(id: Long, isLiked: Boolean) +} diff --git a/core/database/src/main/kotlin/com/unifest/android/core/database/LikedBoothDatabase.kt b/core/database/src/main/kotlin/com/unifest/android/core/database/LikedBoothDatabase.kt new file mode 100644 index 00000000..a4734a0f --- /dev/null +++ b/core/database/src/main/kotlin/com/unifest/android/core/database/LikedBoothDatabase.kt @@ -0,0 +1,16 @@ +package com.unifest.android.core.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.unifest.android.core.database.entity.LikedBoothEntity + +@Database( + entities = [LikedBoothEntity::class], + version = 1, + exportSchema = true, +) +@TypeConverters(MenuListConverter::class) +abstract class LikedBoothDatabase : RoomDatabase() { + abstract fun likedBoothDao(): LikedBoothDao +} diff --git a/core/database/src/main/kotlin/com/unifest/android/core/database/MenuListConverter.kt b/core/database/src/main/kotlin/com/unifest/android/core/database/MenuListConverter.kt new file mode 100644 index 00000000..8f052f3d --- /dev/null +++ b/core/database/src/main/kotlin/com/unifest/android/core/database/MenuListConverter.kt @@ -0,0 +1,20 @@ +package com.unifest.android.core.database + +import androidx.room.TypeConverter +import com.unifest.android.core.database.entity.MenuEntity +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class MenuListConverter { + private val json = Json + + @TypeConverter + fun fromMenuList(menuList: List): String { + return json.encodeToString(menuList) + } + + @TypeConverter + fun toMenuList(menuListString: String): List { + return json.decodeFromString(menuListString) + } +} diff --git a/core/database/src/main/kotlin/com/unifest/android/core/database/di/DaoModule.kt b/core/database/src/main/kotlin/com/unifest/android/core/database/di/DaoModule.kt new file mode 100644 index 00000000..a799949c --- /dev/null +++ b/core/database/src/main/kotlin/com/unifest/android/core/database/di/DaoModule.kt @@ -0,0 +1,17 @@ +package com.unifest.android.core.database.di + +import com.unifest.android.core.database.LikedBoothDao +import com.unifest.android.core.database.LikedBoothDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object DaoModule { + @Provides + fun provideLikedBoothDao( + database: LikedBoothDatabase, + ): LikedBoothDao = database.likedBoothDao() +} diff --git a/core/database/src/main/kotlin/com/unifest/android/core/database/di/DatabaseModule.kt b/core/database/src/main/kotlin/com/unifest/android/core/database/di/DatabaseModule.kt new file mode 100644 index 00000000..736e23bf --- /dev/null +++ b/core/database/src/main/kotlin/com/unifest/android/core/database/di/DatabaseModule.kt @@ -0,0 +1,25 @@ +package com.unifest.android.core.database.di + +import android.content.Context +import androidx.room.Room +import com.unifest.android.core.database.LikedBoothDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Singleton + @Provides + fun provideLikedBoothDatabase(@ApplicationContext context: Context): LikedBoothDatabase = + Room.databaseBuilder( + context.applicationContext, + LikedBoothDatabase::class.java, + "liked_booth_database", + ).build() +} 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 new file mode 100644 index 00000000..5846a02e --- /dev/null +++ b/core/database/src/main/kotlin/com/unifest/android/core/database/entity/LikedBoothEntity.kt @@ -0,0 +1,40 @@ +package com.unifest.android.core.database.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.Serializable + +@Serializable +@Entity(tableName = "liked_booth") +data class LikedBoothEntity( + @PrimaryKey + @ColumnInfo(name = "id") + val id: Long, + @ColumnInfo(name = "name") + val name: String, + @ColumnInfo(name = "category") + val category: String, + @ColumnInfo(name = "description") + val description: String, + @ColumnInfo(name = "warning") + val warning: String, + @ColumnInfo(name = "location") + val location: String, + @ColumnInfo(name = "latitude") + val latitude: Float, + @ColumnInfo(name = "longitude") + val longitude: Float, + @ColumnInfo(name = "menus") + val menus: List, + @ColumnInfo(name = "is_liked") + val isLiked: Boolean, +) + +@Serializable +data class MenuEntity( + val id: Long = 0L, + val name: String = "", + val price: Int = 0, + val imgUrl: String = "", +) 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 edbdd8d0..b4eb2a5b 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 @@ -155,15 +155,15 @@ fun NetworkErrorDialog( } @Composable -fun InterestedFestivalDeleteDialog( +fun LikedFestivalDeleteDialog( onCancelClick: () -> Unit, onConfirmClick: () -> Unit, ) { UnifestDialog( - titleResId = R.string.interested_festival_delete_title, + titleResId = R.string.liked_festival_delete_title, iconResId = R.drawable.ic_caution, iconDescription = "Caution Icon", - descriptionResId = R.string.interested_festival_delete_description, + descriptionResId = R.string.liked_festival_delete_description, confirmTextResId = R.string.confirm, cancelTextResId = R.string.cancel, onCancelClick = onCancelClick, @@ -189,9 +189,9 @@ fun NetworkErrorDialogPreview() { @ComponentPreview @Composable -fun InterestedFestivalDeleteDialogPreview() { +fun LikedFestivalDeleteDialogPreview() { UnifestTheme { - InterestedFestivalDeleteDialog( + LikedFestivalDeleteDialog( onCancelClick = {}, onConfirmClick = {}, ) diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index d4858f15..d2406da6 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -11,7 +11,7 @@ 관심있는 학교 축제를 추가해보세요 관심 학교는 언제든지 수정 가능합니다 학교를 검색해보세요 - 나의 관심 축제 + 나의 관심 축제 모두 선택 해제 전체 @@ -47,20 +47,21 @@ 메뉴 - 나의 관심 학교 + 나의 관심 학교 추가하기> - 관심 부스 + 관심 부스 더보기> 이용 문의 운영자 모드 진입 + 관심 부스 없음 + 지도에서 관심있는 부스를 추가해주세요 - - 관심 부스 - + + 관심 부스 학교 / 축제 이름을 검색해보세요. - 나의 관심 축제 + 나의 관심 축제 편집 완료 추가 @@ -73,8 +74,8 @@ 다시 시도 네트워크 문제 와이파이와 데이터 접속을 확인해주세요 - 관심 축제를 삭제합니다 - 정말 삭제하실 건가요? + 관심 축제를 삭제합니다 + 정말 삭제하실 건가요? 확인 취소 diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index e3bd7166..9729b5ed 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -13,6 +13,8 @@ dependencies { libs.compose.stable.marker, ) implementations( - libs.androidx.core, + projects.core.data, + + libs.javax.inject, ) } diff --git a/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/BoothDetailEntity.kt b/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/BoothDetailEntity.kt index 73db29fa..7fb757d8 100644 --- a/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/BoothDetailEntity.kt +++ b/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/BoothDetailEntity.kt @@ -13,6 +13,7 @@ data class BoothDetailEntity( val latitude: Float = 0f, val longitude: Float = 0f, val menus: List = emptyList(), + val isLiked: Boolean = false, ) @Stable diff --git a/core/domain/src/main/kotlin/com/unifest/android/core/domain/mapper/EntityToResponse.kt b/core/domain/src/main/kotlin/com/unifest/android/core/domain/mapper/EntityToResponse.kt new file mode 100644 index 00000000..cd05a36e --- /dev/null +++ b/core/domain/src/main/kotlin/com/unifest/android/core/domain/mapper/EntityToResponse.kt @@ -0,0 +1,29 @@ +package com.unifest.android.core.domain.mapper + +import com.unifest.android.core.data.response.BoothDetailResponse +import com.unifest.android.core.data.response.MenuResponse +import com.unifest.android.core.domain.entity.BoothDetailEntity +import com.unifest.android.core.domain.entity.MenuEntity + +fun BoothDetailEntity.toResponse(): BoothDetailResponse { + return BoothDetailResponse( + id = id, + name = name, + category = category, + description = description, + warning = warning, + location = location, + latitude = latitude, + longitude = longitude, + menus = menus.map { it.toResponse() }, + ) +} + +fun MenuEntity.toResponse(): MenuResponse { + return MenuResponse( + id = id, + name = name, + price = price, + imgUrl = imgUrl, + ) +} diff --git a/core/domain/src/main/kotlin/com/unifest/android/core/domain/mapper/ResponseToEntity.kt b/core/domain/src/main/kotlin/com/unifest/android/core/domain/mapper/ResponseToEntity.kt new file mode 100644 index 00000000..004cfd7e --- /dev/null +++ b/core/domain/src/main/kotlin/com/unifest/android/core/domain/mapper/ResponseToEntity.kt @@ -0,0 +1,30 @@ +package com.unifest.android.core.domain.mapper + +import com.unifest.android.core.data.response.BoothDetailResponse +import com.unifest.android.core.data.response.MenuResponse +import com.unifest.android.core.domain.entity.BoothDetailEntity +import com.unifest.android.core.domain.entity.MenuEntity + +fun BoothDetailResponse.toEntity(): BoothDetailEntity { + return BoothDetailEntity( + id = id, + name = name, + category = category, + description = description, + warning = warning, + location = location, + latitude = latitude, + longitude = longitude, + menus = menus.map { it.toEntity() }, + isLiked = false, + ) +} + +fun MenuResponse.toEntity(): MenuEntity { + return MenuEntity( + id = id, + name = name, + price = price, + imgUrl = imgUrl, + ) +} 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 new file mode 100644 index 00000000..b79e898e --- /dev/null +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/EmptyLikedBoothItem.kt @@ -0,0 +1,60 @@ +package com.unifest.android.core.ui.component + +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.fillMaxWidth +import androidx.compose.foundation.layout.height +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.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) { + Column( + modifier = Modifier + .fillMaxSize() + .align(Alignment.Center), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(id = R.string.menu_liked_booth_empty), + style = Title2, + color = Color.Black, + ) + Spacer(modifier = Modifier.height(9.dp)) + Text( + text = stringResource(id = R.string.menu_insert_liked_booth), + style = Content6, + color = Color(0xFF848484), + ) + } + } +} + +@ComponentPreview +@Composable +fun EmptyLikedBoothItemPreview() { + UnifestTheme { + EmptyLikedBoothItem( + modifier = Modifier + .fillMaxWidth() + .height(248.dp), + ) + } +} diff --git a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/FestivalSearchBottomSheet.kt b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/FestivalSearchBottomSheet.kt index aae7c46c..c56e3b73 100644 --- a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/FestivalSearchBottomSheet.kt +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/FestivalSearchBottomSheet.kt @@ -31,7 +31,7 @@ import com.skydoves.flexible.core.rememberFlexibleBottomSheetState import com.unifest.android.core.designsystem.ComponentPreview import com.unifest.android.core.designsystem.R import com.unifest.android.core.designsystem.component.FestivalSearchTextField -import com.unifest.android.core.designsystem.component.InterestedFestivalDeleteDialog +import com.unifest.android.core.designsystem.component.LikedFestivalDeleteDialog import com.unifest.android.core.designsystem.theme.Content3 import com.unifest.android.core.designsystem.theme.UnifestTheme import com.unifest.android.core.domain.entity.Festival @@ -45,14 +45,14 @@ fun FestivalSearchBottomSheet( setFestivalSearchBottomSheetVisible: (Boolean) -> Unit, searchText: TextFieldValue, updateSearchText: (TextFieldValue) -> Unit, - interestedFestivals: MutableList, + likedFestivals: MutableList, festivalSearchResults: ImmutableList, initSearchText: () -> Unit, setEnableSearchMode: (Boolean) -> Unit, isSearchMode: Boolean, setEnableEditMode: () -> Unit, - isInterestedFestivalDeleteDialogVisible: Boolean, - setInterestedFestivalDeleteDialogVisible: (Boolean) -> Unit, + isLikedFestivalDeleteDialogVisible: Boolean, + setLikedFestivalDeleteDialogVisible: (Boolean) -> Unit, isEditMode: Boolean = false, ) { val selectedFestivals = remember { mutableStateListOf() } @@ -126,13 +126,13 @@ fun FestivalSearchBottomSheet( .background(Color(0xFFF1F3F7)), ) Spacer(modifier = Modifier.height(21.dp)) - InterestedFestivalsGrid( - selectedFestivals = interestedFestivals, + LikedFestivalsGrid( + selectedFestivals = likedFestivals, onFestivalSelected = { school -> selectedFestivals.remove(school) }, isEditMode = isEditMode, - setInterestedFestivalDeleteDialogVisible = setInterestedFestivalDeleteDialogVisible, + setLikedFestivalDeleteDialogVisible = setLikedFestivalDeleteDialogVisible, ) { TextButton( onClick = setEnableEditMode, @@ -150,13 +150,13 @@ fun FestivalSearchBottomSheet( ) } } - if (isInterestedFestivalDeleteDialogVisible) { - InterestedFestivalDeleteDialog( + if (isLikedFestivalDeleteDialogVisible) { + LikedFestivalDeleteDialog( onCancelClick = { - setInterestedFestivalDeleteDialogVisible(false) + setLikedFestivalDeleteDialogVisible(false) }, onConfirmClick = { - setInterestedFestivalDeleteDialogVisible(false) + setLikedFestivalDeleteDialogVisible(false) }, ) } @@ -173,7 +173,7 @@ fun SchoolSearchBottomSheetPreview() { setFestivalSearchBottomSheetVisible = {}, searchText = TextFieldValue(), updateSearchText = {}, - interestedFestivals = mutableListOf( + likedFestivals = mutableListOf( Festival("https://picsum.photos/36", "서울대학교", "설대축제", "05.06-05.08"), Festival("https://picsum.photos/36", "연세대학교", "연대축제", "05.06-05.08"), Festival("https://picsum.photos/36", "고려대학교", "고대축제", "05.06-05.08"), @@ -191,9 +191,9 @@ fun SchoolSearchBottomSheetPreview() { setEnableSearchMode = {}, isSearchMode = false, setEnableEditMode = {}, - isInterestedFestivalDeleteDialogVisible = false, + isLikedFestivalDeleteDialogVisible = false, isEditMode = false, - setInterestedFestivalDeleteDialogVisible = {}, + setLikedFestivalDeleteDialogVisible = {}, ) } } diff --git a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/InterestedFestivalGrid.kt b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/InterestedFestivalGrid.kt index 78bc8f40..91f21437 100644 --- a/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/InterestedFestivalGrid.kt +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/InterestedFestivalGrid.kt @@ -40,11 +40,11 @@ import com.unifest.android.core.designsystem.theme.UnifestTheme import com.unifest.android.core.domain.entity.Festival @Composable -fun InterestedFestivalsGrid( +fun LikedFestivalsGrid( selectedFestivals: MutableList, onFestivalSelected: (Festival) -> Unit, isEditMode: Boolean = false, - setInterestedFestivalDeleteDialogVisible: (Boolean) -> Unit = {}, + setLikedFestivalDeleteDialogVisible: (Boolean) -> Unit = {}, optionTextButton: @Composable () -> Unit, ) { Column { @@ -56,7 +56,7 @@ fun InterestedFestivalsGrid( .padding(horizontal = 20.dp), ) { Text( - text = stringResource(id = R.string.intro_interested_festivals_title), + text = stringResource(id = R.string.intro_liked_festivals_title), style = Title3, ) optionTextButton() @@ -88,7 +88,7 @@ fun InterestedFestivalsGrid( onFestivalSelected(it) }, isEditMode = isEditMode, - setInterestedFestivalDeleteDialogVisible = setInterestedFestivalDeleteDialogVisible, + setLikedFestivalDeleteDialogVisible = setLikedFestivalDeleteDialogVisible, ) } } @@ -100,7 +100,7 @@ fun FestivalItem( festival: Festival, onFestivalSelected: (Festival) -> Unit, isEditMode: Boolean = false, - setInterestedFestivalDeleteDialogVisible: (Boolean) -> Unit = {}, + setLikedFestivalDeleteDialogVisible: (Boolean) -> Unit = {}, ) { Card( shape = RoundedCornerShape(16.dp), @@ -112,7 +112,7 @@ fun FestivalItem( modifier = Modifier .clickable { if (isEditMode) { - setInterestedFestivalDeleteDialogVisible(true) + setLikedFestivalDeleteDialogVisible(true) } else { onFestivalSelected(festival) } @@ -166,7 +166,7 @@ fun FestivalItem( @ComponentPreview @Composable -fun InterestedFestivalsGridPreview() { +fun LikedFestivalsGridPreview() { val selectedFestivals = mutableListOf() repeat(5) { selectedFestivals.add( @@ -179,7 +179,7 @@ fun InterestedFestivalsGridPreview() { ) } UnifestTheme { - InterestedFestivalsGrid( + LikedFestivalsGrid( selectedFestivals = mutableListOf( Festival( schoolName = "건국대학교", @@ -225,7 +225,7 @@ fun InterestedFestivalsGridPreview() { @ComponentPreview @Composable -fun InterestedFestivalsGridEditModePreview() { +fun LikedFestivalsGridEditModePreview() { val selectedFestivals = mutableListOf() repeat(5) { selectedFestivals.add( @@ -238,7 +238,7 @@ fun InterestedFestivalsGridEditModePreview() { ) } UnifestTheme { - InterestedFestivalsGrid( + LikedFestivalsGrid( selectedFestivals = mutableListOf( Festival( schoolName = "건국대학교", 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 new file mode 100644 index 00000000..8793caa3 --- /dev/null +++ b/core/ui/src/main/kotlin/com/unifest/android/core/ui/component/LikedBoothItem.kt @@ -0,0 +1,134 @@ +package com.unifest.android.core.ui.component + +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.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.designsystem.ComponentPreview +import com.unifest.android.core.designsystem.R +import com.unifest.android.core.designsystem.component.NetworkImage +import com.unifest.android.core.designsystem.theme.Title2 +import com.unifest.android.core.designsystem.theme.Title5 +import com.unifest.android.core.domain.entity.BoothDetailEntity + +@Composable +fun LikedBoothItem( + booth: BoothDetailEntity, + index: Int, + totalCount: Int, + deleteLikedBooth: (BoothDetailEntity) -> Unit, + modifier: Modifier = Modifier, +) { + val bookMarkColor = if (booth.isLiked) Color(0xFFF5687E) else Color(0xFF4B4B4B) + Column( + modifier = modifier + .clickable { /* 클릭 이벤트 처리 */ } + .padding(horizontal = 20.dp), + ) { + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxSize(), + ) { + NetworkImage( + imageUrl = "https://picsum.photos/86", + modifier = Modifier + .size(86.dp) + .clip(RoundedCornerShape(16.dp)), + ) + Spacer(modifier = Modifier.width(14.dp)) + Column( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { + Text( + text = booth.name, + style = Title2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = booth.category, + style = Title5, + color = Color(0xFF545454), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier.height(13.dp)) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = R.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), + ) + } + } + Icon( + imageVector = ImageVector.vectorResource(if (booth.isLiked) R.drawable.ic_bookmarked else R.drawable.ic_bookmark), + contentDescription = "Bookmark Icon", + tint = bookMarkColor, + modifier = Modifier + .size(24.dp) + .clickable( + onClick = { + deleteLikedBooth(booth) + }, + ), + ) + } + Spacer(modifier = Modifier.height(16.dp)) + if (index < totalCount - 1) { + HorizontalDivider( + color = Color(0xFFDFDFDF), + thickness = 1.dp, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@ComponentPreview +@Composable +fun LikedBoothItemPreview() { + LikedBoothItem( + booth = BoothDetailEntity( + id = 1, + name = "부스 이름", + category = "부스 카테고리", + description = "부스 설명", + warning = "", + location = "부스 위치", + isLiked = true, + ), + index = 0, + totalCount = 1, + deleteLikedBooth = {}, + ) +} diff --git a/feature/booth/build.gradle.kts b/feature/booth/build.gradle.kts index e09cb7bd..cdcb9dd7 100644 --- a/feature/booth/build.gradle.kts +++ b/feature/booth/build.gradle.kts @@ -11,8 +11,10 @@ android { dependencies { implementations( + projects.core.data, + projects.core.domain, + libs.kotlinx.collections.immutable, - libs.androidx.core, libs.compose.system.ui.controller, libs.timber, 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 2e334dbb..d07ab658 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 @@ -73,7 +73,13 @@ internal fun BoothDetailRoute( darkIcons = false, isNavigationBarContrastEnforced = false, ) - onDispose {} + onDispose { + systemUiController.setSystemBarsColor( + color = Color.White, + darkIcons = true, + isNavigationBarContrastEnforced = false, + ) + } } BoothDetailScreen( 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 cf0a8ac6..2a3b2417 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 @@ -17,7 +17,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -44,7 +43,6 @@ import com.unifest.android.core.designsystem.theme.UnifestTheme import com.unifest.android.core.domain.entity.BoothDetailEntity import com.unifest.android.feature.booth.viewmodel.BoothUiState import com.unifest.android.feature.booth.viewmodel.BoothViewModel -import tech.thdev.compose.exteions.system.ui.controller.rememberExSystemUiController @Composable fun BoothLocationRoute( @@ -52,16 +50,6 @@ fun BoothLocationRoute( viewModel: BoothViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val systemUiController = rememberExSystemUiController() - - DisposableEffect(systemUiController) { - systemUiController.setSystemBarsColor( - color = Color.White, - darkIcons = true, - isNavigationBarContrastEnforced = false, - ) - onDispose {} - } BoothLocationScreen( uiState = uiState, 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 a82d690e..aea780fd 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 @@ -2,18 +2,23 @@ package com.unifest.android.feature.booth.viewmodel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.unifest.android.core.data.repository.LikedBoothRepository import com.unifest.android.core.domain.entity.BoothDetailEntity import com.unifest.android.core.domain.entity.MenuEntity +import com.unifest.android.core.domain.mapper.toResponse import com.unifest.android.feature.booth.navigation.BOOTH_ID 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 kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class BoothViewModel @Inject constructor( + private val likedBoothRepository: LikedBoothRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { @Suppress("unused") @@ -45,12 +50,19 @@ class BoothViewModel @Inject constructor( } fun toggleBookmark() { - _uiState.update { currentState -> - val newBookmarkState = !currentState.isBookmarked - currentState.copy( - isBookmarked = newBookmarkState, - bookmarkCount = currentState.bookmarkCount + if (newBookmarkState) 1 else -1, - ) + viewModelScope.launch { + if (_uiState.value.isBookmarked) { + likedBoothRepository.deleteLikedBooth(_uiState.value.boothDetailInfo.toResponse()) + } else { + likedBoothRepository.insertLikedBooth(_uiState.value.boothDetailInfo.toResponse()) + } + _uiState.update { currentState -> + val newBookmarkState = !currentState.isBookmarked + currentState.copy( + isBookmarked = newBookmarkState, + bookmarkCount = currentState.bookmarkCount + if (newBookmarkState) 1 else -1, + ) + } } } } diff --git a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/navigation/InterestedBoothNavigation.kt b/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/navigation/InterestedBoothNavigation.kt deleted file mode 100644 index a4437a5b..00000000 --- a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/navigation/InterestedBoothNavigation.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.unifest.android.feature.interested_booth.navigation - -import androidx.compose.foundation.layout.PaddingValues -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.unifest.android.feature.interested_booth.InterestedBoothRoute - -const val INTERESTED_BOOTH_ROUTE = "interested_booth_route" - -fun NavController.navigateToInterestedBooth() { - navigate(INTERESTED_BOOTH_ROUTE) -} - -fun NavGraphBuilder.interestedBoothNavGraph( - padding: PaddingValues, - onBackClick: () -> Unit, -) { - composable(route = INTERESTED_BOOTH_ROUTE) { - InterestedBoothRoute( - padding = padding, - onBackClick = onBackClick, - ) - } -} diff --git a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/viewmodel/InterestedBoothViewModel.kt b/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/viewmodel/InterestedBoothViewModel.kt deleted file mode 100644 index 1f1c06e2..00000000 --- a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/viewmodel/InterestedBoothViewModel.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.unifest.android.feature.interested_booth.viewmodel - -import androidx.lifecycle.ViewModel -import com.unifest.android.core.domain.entity.BoothDetailEntity -import com.unifest.android.core.domain.entity.MenuEntity -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.persistentListOf -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 InterestedBoothViewModel @Inject constructor() : ViewModel() { - private val _uiState = MutableStateFlow(InterestedBoothUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - init { - _uiState.update { - it.copy( - interestedBooths = persistentListOf( - BoothDetailEntity( - id = 1, - name = "부스 이름", - category = "음식", - description = "부스 설명", - warning = "주의사항", - location = "부스 위치", - latitude = 0.0f, - longitude = 0.0f, - menus = listOf( - MenuEntity( - id = 1, - name = "메뉴 이름", - price = 1000, - imgUrl = "", - ), - ), - ), - BoothDetailEntity( - id = 2, - name = "부스 이름", - category = "음식", - description = "부스 설명", - warning = "주의사항", - location = "부스 위치", - latitude = 0.0f, - longitude = 0.0f, - menus = listOf( - MenuEntity( - id = 1, - name = "메뉴 이름", - price = 1000, - imgUrl = "", - ), - ), - ), - BoothDetailEntity( - id = 3, - name = "부스 이름", - category = "음식", - description = "부스 설명", - warning = "주의사항", - location = "부스 위치", - latitude = 0.0f, - longitude = 0.0f, - menus = listOf( - MenuEntity( - id = 1, - name = "메뉴 이름", - price = 1000, - imgUrl = "", - ), - ), - ), - BoothDetailEntity( - id = 4, - name = "부스 이름", - category = "음식", - description = "부스 설명", - warning = "주의사항", - location = "부스 위치", - latitude = 0.0f, - longitude = 0.0f, - menus = listOf( - MenuEntity( - id = 1, - name = "메뉴 이름", - price = 1000, - imgUrl = "", - ), - ), - ), - BoothDetailEntity( - id = 5, - name = "부스 이름", - category = "음식", - description = "부스 설명", - warning = "주의사항", - location = "부스 위치", - latitude = 0.0f, - longitude = 0.0f, - menus = listOf( - MenuEntity( - id = 1, - name = "메뉴 이름", - price = 1000, - imgUrl = "", - ), - ), - ), - BoothDetailEntity( - id = 6, - name = "부스 이름", - category = "음식", - description = "부스 설명", - warning = "주의사항", - location = "부스 위치", - latitude = 0.0f, - longitude = 0.0f, - menus = listOf( - MenuEntity( - id = 1, - name = "메뉴 이름", - price = 1000, - imgUrl = "", - ), - ), - ), - ), - ) - } - } -} 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 a75e31a7..eeece62c 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 @@ -49,7 +49,7 @@ import com.unifest.android.core.designsystem.theme.UnifestTheme import com.unifest.android.core.domain.entity.Festival import com.unifest.android.core.ui.DevicePreview import com.unifest.android.core.ui.component.FestivalItem -import com.unifest.android.core.ui.component.InterestedFestivalsGrid +import com.unifest.android.core.ui.component.LikedFestivalsGrid import com.unifest.android.feature.intro.viewmodel.IntroUiState import com.unifest.android.feature.intro.viewmodel.IntroViewModel import kotlinx.collections.immutable.ImmutableList @@ -104,7 +104,7 @@ fun IntroScreen( .padding(horizontal = 20.dp), ) Spacer(modifier = Modifier.height(18.dp)) - InterestedFestivalsGrid( + LikedFestivalsGrid( selectedFestivals = selectedFestivals, onFestivalSelected = { school -> selectedFestivals.remove(school) diff --git a/feature/liked-booth/.gitignore b/feature/liked-booth/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/liked-booth/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/interested-booth/build.gradle.kts b/feature/liked-booth/build.gradle.kts similarity index 73% rename from feature/interested-booth/build.gradle.kts rename to feature/liked-booth/build.gradle.kts index 799aa504..29f0b1ac 100644 --- a/feature/interested-booth/build.gradle.kts +++ b/feature/liked-booth/build.gradle.kts @@ -6,13 +6,15 @@ plugins { } android { - namespace = "com.unifest.android.feature.interested_booth" + namespace = "com.unifest.android.feature.liked_booth" } dependencies { implementations( + projects.core.data, + projects.core.domain, + libs.kotlinx.collections.immutable, - libs.androidx.core, libs.compose.system.ui.controller, libs.timber, ) diff --git a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/InterestedBoothScreen.kt b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/LikedBoothScreen.kt similarity index 61% rename from feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/InterestedBoothScreen.kt rename to feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/LikedBoothScreen.kt index 4bf87204..54885886 100644 --- a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/InterestedBoothScreen.kt +++ b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/LikedBoothScreen.kt @@ -1,72 +1,61 @@ -package com.unifest.android.feature.interested_booth +package com.unifest.android.feature.liked_booth +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.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.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -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.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.unifest.android.core.designsystem.R -import com.unifest.android.core.designsystem.component.NetworkImage import com.unifest.android.core.designsystem.component.TopAppBarNavigationType import com.unifest.android.core.designsystem.component.UnifestTopAppBar -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.domain.entity.BoothDetailEntity import com.unifest.android.core.domain.entity.MenuEntity import com.unifest.android.core.ui.DevicePreview -import com.unifest.android.feature.interested_booth.viewmodel.InterestedBoothUiState -import com.unifest.android.feature.interested_booth.viewmodel.InterestedBoothViewModel +import com.unifest.android.core.ui.component.EmptyLikedBoothItem +import com.unifest.android.core.ui.component.LikedBoothItem +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 InterestedBoothRoute( +internal fun LikedBoothRoute( padding: PaddingValues, onBackClick: () -> Unit, - viewModel: InterestedBoothViewModel = hiltViewModel(), + viewModel: LikedBoothViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - InterestedBoothScreen( + + LikedBoothScreen( padding = padding, uiState = uiState, onBackClick = onBackClick, + deleteLikedBooth = viewModel::deleteLikedBooth, ) } +@OptIn(ExperimentalFoundationApi::class) @Composable -internal fun InterestedBoothScreen( +internal fun LikedBoothScreen( padding: PaddingValues, - uiState: InterestedBoothUiState, + uiState: LikedBoothUiState, onBackClick: () -> Unit, + deleteLikedBooth: (BoothDetailEntity) -> Unit, ) { Box( modifier = Modifier @@ -77,7 +66,7 @@ internal fun InterestedBoothScreen( UnifestTopAppBar( navigationType = TopAppBarNavigationType.Back, onNavigationClick = onBackClick, - title = stringResource(id = R.string.interested_booths_title), + title = stringResource(id = R.string.liked_booth_title), elevation = 8.dp, modifier = Modifier .background( @@ -86,98 +75,40 @@ internal fun InterestedBoothScreen( ) .padding(top = 13.dp, bottom = 5.dp), ) + if (uiState.likedBoothList.isEmpty()) { + EmptyLikedBoothItem(modifier = Modifier.fillMaxSize()) + } LazyColumn { itemsIndexed( - uiState.interestedBooths, + uiState.likedBoothList, key = { _, booth -> booth.id }, ) { index, booth -> - InterestedBoothsItems(booth, index, uiState.interestedBooths.size) - } - } - } - } -} - -@Composable -fun InterestedBoothsItems(booth: BoothDetailEntity, index: Int, total: Int) { - var isBookmarked by remember { mutableStateOf(true) } - val bookMarkColor = if (isBookmarked) Color(0xFFF5687E) else Color(0xFF4B4B4B) - Column( - modifier = Modifier - .clickable { /* 클릭 이벤트 처리 */ } - .padding(horizontal = 20.dp), - ) { - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxSize(), - ) { - NetworkImage( - imageUrl = "https://picsum.photos/86", - modifier = Modifier - .size(86.dp) - .clip(RoundedCornerShape(16.dp)), - ) - Spacer(modifier = Modifier.width(14.dp)) - Column( - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - ) { - Text( - text = booth.name, - style = Title2, - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = booth.description, - style = Title5, - color = Color(0xFF545454), - ) - Spacer(modifier = Modifier.height(13.dp)) - Row { - Icon( - imageVector = ImageVector.vectorResource(id = R.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), + LikedBoothItem( + booth = booth, + index = index, + totalCount = uiState.likedBoothList.size, + deleteLikedBooth = { deleteLikedBooth(booth) }, + modifier = Modifier.animateItemPlacement( + animationSpec = tween( + durationMillis = 500, + easing = LinearOutSlowInEasing, + ), + ), ) } } - Icon( - imageVector = ImageVector.vectorResource(if (isBookmarked) R.drawable.ic_bookmarked else R.drawable.ic_bookmark), - contentDescription = if (isBookmarked) "북마크됨" else "북마크하기", - tint = bookMarkColor, - modifier = Modifier - .size(24.dp) - .clickable(onClick = { isBookmarked = !isBookmarked }), - ) - } - Spacer(modifier = Modifier.height(16.dp)) - if (index < total - 1) { - HorizontalDivider( - color = Color(0xFFDFDFDF), - thickness = 1.dp, - modifier = Modifier.fillMaxWidth(), - ) } } } @DevicePreview @Composable -fun InterestedBoothScreenPreview() { +fun LikedBoothScreenPreview() { UnifestTheme { - InterestedBoothScreen( + LikedBoothScreen( padding = PaddingValues(), - onBackClick = {}, - uiState = InterestedBoothUiState( - interestedBooths = persistentListOf( + uiState = LikedBoothUiState( + likedBoothList = persistentListOf( BoothDetailEntity( id = 1, name = "부스 이름", @@ -288,6 +219,8 @@ fun InterestedBoothScreenPreview() { ), ), ), + onBackClick = {}, + deleteLikedBooth = {}, ) } } diff --git a/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/navigation/InterestedBoothNavigation.kt b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/navigation/InterestedBoothNavigation.kt new file mode 100644 index 00000000..df414a14 --- /dev/null +++ b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/navigation/InterestedBoothNavigation.kt @@ -0,0 +1,25 @@ +package com.unifest.android.feature.liked_booth.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import com.unifest.android.feature.liked_booth.LikedBoothRoute + +const val Liked_BOOTH_ROUTE = "liked_booth_route" + +fun NavController.navigateToLikedBoothList() { + navigate(Liked_BOOTH_ROUTE) +} + +fun NavGraphBuilder.likedBoothNavGraph( + padding: PaddingValues, + onBackClick: () -> Unit, +) { + composable(route = Liked_BOOTH_ROUTE) { + LikedBoothRoute( + padding = padding, + onBackClick = onBackClick, + ) + } +} diff --git a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/viewmodel/InterestedBoothUiState.kt b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothUiState.kt similarity index 53% rename from feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/viewmodel/InterestedBoothUiState.kt rename to feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothUiState.kt index 6c52e1b0..eb8ed719 100644 --- a/feature/interested-booth/src/main/kotlin/com/unifest/android/feature/interested_booth/viewmodel/InterestedBoothUiState.kt +++ b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothUiState.kt @@ -1,10 +1,10 @@ -package com.unifest.android.feature.interested_booth.viewmodel +package com.unifest.android.feature.liked_booth.viewmodel import com.unifest.android.core.domain.entity.BoothDetailEntity import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -data class InterestedBoothUiState( +data class LikedBoothUiState( val isLoading: Boolean = false, - val interestedBooths: ImmutableList = persistentListOf(), + val likedBoothList: ImmutableList = persistentListOf(), ) 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 new file mode 100644 index 00000000..c0d4475b --- /dev/null +++ b/feature/liked-booth/src/main/kotlin/com/unifest/android/feature/liked_booth/viewmodel/LikedBoothViewModel.kt @@ -0,0 +1,167 @@ +package com.unifest.android.feature.liked_booth.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.unifest.android.core.data.repository.LikedBoothRepository +import com.unifest.android.core.domain.entity.BoothDetailEntity +import com.unifest.android.core.domain.mapper.toEntity +import com.unifest.android.core.domain.mapper.toResponse +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LikedBoothViewModel @Inject constructor( + private val likedBoothRepository: LikedBoothRepository, +) : ViewModel() { + private val _uiState = MutableStateFlow(LikedBoothUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch { + likedBoothRepository.getLikedBoothList().collect { likedBoothList -> + _uiState.update { + it.copy( + likedBoothList = likedBoothList.map { likedBooth -> + likedBooth.toEntity() + }.toImmutableList(), + ) + } + } + } + } + + fun deleteLikedBooth(booth: BoothDetailEntity) { + viewModelScope.launch { + updateLikedBooth(booth) + delay(500) + likedBoothRepository.deleteLikedBooth(booth.toResponse()) + } + } + + private suspend fun updateLikedBooth(booth: BoothDetailEntity) { + likedBoothRepository.updateLikedBooth(booth.copy(isLiked = false).toResponse()) + } + +// init { +// _uiState.update { +// it.copy( +// likedBoothList = persistentListOf( +// BoothDetailEntity( +// id = 1, +// name = "부스 이름", +// category = "음식", +// description = "부스 설명", +// warning = "주의사항", +// location = "부스 위치", +// latitude = 0.0f, +// longitude = 0.0f, +// menus = listOf( +// MenuEntity( +// id = 1, +// name = "메뉴 이름", +// price = 1000, +// imgUrl = "", +// ), +// ), +// ), +// BoothDetailEntity( +// id = 2, +// name = "부스 이름", +// category = "음식", +// description = "부스 설명", +// warning = "주의사항", +// location = "부스 위치", +// latitude = 0.0f, +// longitude = 0.0f, +// menus = listOf( +// MenuEntity( +// id = 1, +// name = "메뉴 이름", +// price = 1000, +// imgUrl = "", +// ), +// ), +// ), +// BoothDetailEntity( +// id = 3, +// name = "부스 이름", +// category = "음식", +// description = "부스 설명", +// warning = "주의사항", +// location = "부스 위치", +// latitude = 0.0f, +// longitude = 0.0f, +// menus = listOf( +// MenuEntity( +// id = 1, +// name = "메뉴 이름", +// price = 1000, +// imgUrl = "", +// ), +// ), +// ), +// BoothDetailEntity( +// id = 4, +// name = "부스 이름", +// category = "음식", +// description = "부스 설명", +// warning = "주의사항", +// location = "부스 위치", +// latitude = 0.0f, +// longitude = 0.0f, +// menus = listOf( +// MenuEntity( +// id = 1, +// name = "메뉴 이름", +// price = 1000, +// imgUrl = "", +// ), +// ), +// ), +// BoothDetailEntity( +// id = 5, +// name = "부스 이름", +// category = "음식", +// description = "부스 설명", +// warning = "주의사항", +// location = "부스 위치", +// latitude = 0.0f, +// longitude = 0.0f, +// menus = listOf( +// MenuEntity( +// id = 1, +// name = "메뉴 이름", +// price = 1000, +// imgUrl = "", +// ), +// ), +// ), +// BoothDetailEntity( +// id = 6, +// name = "부스 이름", +// category = "음식", +// description = "부스 설명", +// warning = "주의사항", +// location = "부스 위치", +// latitude = 0.0f, +// longitude = 0.0f, +// menus = listOf( +// MenuEntity( +// id = 1, +// name = "메뉴 이름", +// price = 1000, +// imgUrl = "", +// ), +// ), +// ), +// ), +// ) +// } +} diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index ba199efb..1e196ab4 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { implementations( projects.feature.booth, projects.feature.home, - projects.feature.interestedBooth, + projects.feature.likedBooth, projects.feature.intro, projects.feature.map, projects.feature.menu, 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 236f3b17..bc34bb0c 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 @@ -13,7 +13,7 @@ import com.unifest.android.feature.booth.navigation.navigateToBoothDetail import com.unifest.android.feature.booth.navigation.navigateToBoothLocation import com.unifest.android.feature.home.navigation.HOME_ROUTE import com.unifest.android.feature.home.navigation.navigateToHome -import com.unifest.android.feature.interested_booth.navigation.navigateToInterestedBooth +import com.unifest.android.feature.liked_booth.navigation.navigateToLikedBoothList import com.unifest.android.feature.map.navigation.navigateToMap import com.unifest.android.feature.menu.navigation.navigateToMenu import com.unifest.android.feature.waiting.navigation.navigateToWaiting @@ -57,8 +57,8 @@ internal class MainNavController( navController.navigateToBoothLocation() } - fun navigateToInterestedBooth() { - navController.navigateToInterestedBooth() + fun navigateToLikedBoothList() { + navController.navigateToLikedBoothList() } private fun popBackStack() { 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 0032ba79..84e0940f 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 @@ -39,7 +39,7 @@ 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.interested_booth.navigation.interestedBoothNavGraph +import com.unifest.android.feature.liked_booth.navigation.likedBoothNavGraph import com.unifest.android.feature.map.navigation.mapNavGraph import com.unifest.android.feature.menu.navigation.menuNavGraph import com.unifest.android.feature.waiting.navigation.waitingNavGraph @@ -107,9 +107,9 @@ internal fun MainScreen( ) menuNavGraph( padding = innerPadding, - onNavigateToInterestedBooths = navigator::navigateToInterestedBooth, + onNavigateToLikedBooth = navigator::navigateToLikedBoothList, ) - interestedBoothNavGraph( + likedBoothNavGraph( padding = innerPadding, onBackClick = navigator::popBackStackIfNotHome, ) 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 ff19a19e..fb08f482 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 @@ -29,7 +29,6 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -76,7 +75,6 @@ import com.unifest.android.feature.map.viewmodel.MapViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import tech.thdev.compose.exteions.system.ui.controller.rememberExSystemUiController import ted.gun0912.clustering.naver.TedNaverClustering @Composable @@ -86,16 +84,6 @@ internal fun MapRoute( viewModel: MapViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val systemUiController = rememberExSystemUiController() - - DisposableEffect(systemUiController) { - systemUiController.setSystemBarsColor( - color = Color.White, - darkIcons = true, - isNavigationBarContrastEnforced = false, - ) - onDispose {} - } MapScreen( padding = padding, @@ -109,8 +97,8 @@ internal fun MapRoute( setEnableEditMode = viewModel::setEnableEditMode, setEnablePopularMode = viewModel::setEnablePopularMode, setBoothSelectionMode = viewModel::setBoothSelectionMode, - updateSelectedBooths = viewModel::updateSelectedBooths, - setInterestedFestivalDeleteDialogVisible = viewModel::setInterestedFestivalDeleteDialogVisible, + updateSelectedBoothList = viewModel::updateSelectedBoothList, + setLikedFestivalDeleteDialogVisible = viewModel::setLikedFestivalDeleteDialogVisible, ) } @@ -131,8 +119,8 @@ internal fun MapScreen( setEnableEditMode: () -> Unit, setEnablePopularMode: () -> Unit, setBoothSelectionMode: (Boolean) -> Unit, - updateSelectedBooths: (List) -> Unit, - setInterestedFestivalDeleteDialogVisible: (Boolean) -> Unit, + updateSelectedBoothList: (List) -> Unit, + setLikedFestivalDeleteDialogVisible: (Boolean) -> Unit, ) { val rotationState by animateFloatAsState(targetValue = if (uiState.isPopularMode) 180f else 0f) @@ -146,7 +134,7 @@ internal fun MapScreen( val cameraPositionState = rememberCameraPositionState { position = CameraPosition(LatLng(37.540470588662664, 127.0765263757882), 14.0) } - val pagerState = rememberPagerState(pageCount = { uiState.selectedBooths.size }) + val pagerState = rememberPagerState(pageCount = { uiState.selectedBoothList.size }) Box { // TODO 같은 속성의 Marker 들만 클러스터링 되도록 구현 // TODO 클러스터링 마커 커스텀 @@ -162,7 +150,7 @@ internal fun MapScreen( ) { val context = LocalContext.current var clusterManager by remember { mutableStateOf?>(null) } - DisposableMapEffect(uiState.booths) { map -> + DisposableMapEffect(uiState.boothList) { map -> if (clusterManager == null) { clusterManager = TedNaverClustering.with(context, map) .customMarker { @@ -172,17 +160,17 @@ internal fun MapScreen( } .markerClickListener { booth -> setBoothSelectionMode(true) - updateSelectedBooths(listOf(booth)) + updateSelectedBoothList(listOf(booth)) } .clusterClickListener { booths -> setBoothSelectionMode(true) - updateSelectedBooths(booths.items.toList()) + updateSelectedBoothList(booths.items.toList()) } - // 마커를 클릭 했을 경우 지도의 가운데가 마커로 이동 비활성화 + // 마커를 클릭 했을 경우 마커의 위치로 카메라 이동 비활성화 .clickToCenter(false) .make() } - clusterManager?.addItems(uiState.booths) + clusterManager?.addItems(uiState.boothList) onDispose { clusterManager?.clearItems() } @@ -242,7 +230,7 @@ internal fun MapScreen( AnimatedVisibility(uiState.isPopularMode || uiState.isBoothSelectionMode) { BoothCards( pagerState = pagerState, - booths = uiState.booths, + boothList = uiState.boothList, onNavigateToBooth = onNavigateToBooth, isPopularMode = uiState.isPopularMode, modifier = Modifier.wrapContentHeight(), @@ -256,14 +244,14 @@ internal fun MapScreen( updateSearchText = updateFestivalSearchText, searchTextHintRes = R.string.festival_search_text_field_hint, setFestivalSearchBottomSheetVisible = setFestivalSearchBottomSheetVisible, - interestedFestivals = uiState.interestedFestivals, + likedFestivals = uiState.likedFestivals, festivalSearchResults = uiState.festivalSearchResults, initSearchText = initSearchText, setEnableSearchMode = setEnableSearchMode, isSearchMode = uiState.isSearchMode, setEnableEditMode = setEnableEditMode, - isInterestedFestivalDeleteDialogVisible = uiState.isInterestedFestivalDeleteDialogVisible, - setInterestedFestivalDeleteDialogVisible = setInterestedFestivalDeleteDialogVisible, + isLikedFestivalDeleteDialogVisible = uiState.isLikedFestivalDeleteDialogVisible, + setLikedFestivalDeleteDialogVisible = setLikedFestivalDeleteDialogVisible, isEditMode = uiState.isEditMode, ) } @@ -324,7 +312,7 @@ fun MapTopAppBar( @Composable fun BoothCards( pagerState: PagerState, - booths: ImmutableList, + boothList: ImmutableList, onNavigateToBooth: (Long) -> Unit, isPopularMode: Boolean, modifier: Modifier = Modifier, @@ -335,7 +323,7 @@ fun BoothCards( contentPadding = PaddingValues(horizontal = 30.dp), ) { page -> BoothCard( - boothInfo = booths[page], + boothInfo = boothList[page], onNavigateToBooth = onNavigateToBooth, isPopularMode = isPopularMode, ranking = page + 1, @@ -451,7 +439,7 @@ fun MapScreenPreview() { padding = PaddingValues(0.dp), uiState = MapUiState( selectedSchoolName = "건국대학교", - booths = persistentListOf( + boothList = persistentListOf( BoothDetailModel( id = 1L, name = "컴공 주점", @@ -472,8 +460,8 @@ fun MapScreenPreview() { setEnableEditMode = {}, setEnablePopularMode = {}, setBoothSelectionMode = {}, - updateSelectedBooths = {}, - setInterestedFestivalDeleteDialogVisible = {}, + updateSelectedBoothList = {}, + setLikedFestivalDeleteDialogVisible = {}, ) } } @@ -515,7 +503,7 @@ fun BoothCardsPreview() { UnifestTheme { BoothCards( pagerState = rememberPagerState(pageCount = { boothList.size }), - booths = boothList.toImmutableList(), + boothList = boothList.toImmutableList(), onNavigateToBooth = {}, isPopularMode = false, modifier = Modifier.height(116.dp), 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 6cb0cdc0..2a614ffa 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 @@ -8,16 +8,16 @@ import kotlinx.collections.immutable.persistentListOf data class MapUiState( val selectedSchoolName: String = "", - val booths: ImmutableList = persistentListOf(), - val selectedBooths: ImmutableList = persistentListOf(), + val boothList: ImmutableList = persistentListOf(), + val selectedBoothList: ImmutableList = persistentListOf(), val boothSearchText: TextFieldValue = TextFieldValue(), val festivalSearchText: TextFieldValue = TextFieldValue(), - val interestedFestivals: MutableList = mutableListOf(), + val likedFestivals: MutableList = mutableListOf(), val festivalSearchResults: ImmutableList = persistentListOf(), val isSearchMode: Boolean = false, val isEditMode: Boolean = false, val isPopularMode: Boolean = false, val isBoothSelectionMode: Boolean = false, val isFestivalSearchBottomSheetVisible: Boolean = false, - val isInterestedFestivalDeleteDialogVisible: Boolean = false, + val isLikedFestivalDeleteDialogVisible: Boolean = false, ) 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 fbe99160..485ea922 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 @@ -21,7 +21,7 @@ class MapViewModel @Inject constructor() : ViewModel() { val uiState: StateFlow = this._uiState.asStateFlow() init { - val booths = listOf( + val boothList = listOf( BoothDetailEntity( id = 1L, name = "컴공 주점", @@ -100,11 +100,11 @@ class MapViewModel @Inject constructor() : ViewModel() { _uiState.update { it.copy( selectedSchoolName = "건국대학교", - booths = booths + boothList = boothList .map { booth -> booth.toModel() } .toImmutableList(), - selectedBooths = persistentListOf(), - interestedFestivals = mutableListOf( + selectedBoothList = persistentListOf(), + likedFestivals = mutableListOf( Festival("https://picsum.photos/36", "서울대학교", "설대축제", "05.06-05.08"), Festival("https://picsum.photos/36", "연세대학교", "연대축제", "05.06-05.08"), Festival("https://picsum.photos/36", "고려대학교", "고대축제", "05.06-05.08"), @@ -159,7 +159,7 @@ class MapViewModel @Inject constructor() : ViewModel() { } fun setEnablePopularMode() { - val popularBooths = listOf( + val popularBoothList = listOf( BoothDetailEntity( id = 1L, name = "컴공 주점", @@ -210,7 +210,7 @@ class MapViewModel @Inject constructor() : ViewModel() { _uiState.update { it.copy( - selectedBooths = popularBooths + selectedBoothList = popularBoothList .map { popularBooth -> popularBooth.toModel() } .toImmutableList(), isPopularMode = !_uiState.value.isPopularMode, @@ -228,15 +228,15 @@ class MapViewModel @Inject constructor() : ViewModel() { } } - fun updateSelectedBooths(booths: List) { + fun updateSelectedBoothList(boothList: List) { _uiState.update { - it.copy(selectedBooths = booths.toImmutableList()) + it.copy(selectedBoothList = boothList.toImmutableList()) } } - fun setInterestedFestivalDeleteDialogVisible(flag: Boolean) { + fun setLikedFestivalDeleteDialogVisible(flag: Boolean) { _uiState.update { - it.copy(isInterestedFestivalDeleteDialogVisible = flag) + it.copy(isLikedFestivalDeleteDialogVisible = flag) } } } diff --git a/feature/menu/build.gradle.kts b/feature/menu/build.gradle.kts index 1d04783a..d004ab40 100644 --- a/feature/menu/build.gradle.kts +++ b/feature/menu/build.gradle.kts @@ -11,8 +11,10 @@ android { dependencies { implementations( + projects.core.data, + projects.core.domain, + libs.kotlinx.collections.immutable, - libs.androidx.core, libs.timber, ) } 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 832d2931..d289ff33 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 @@ -1,16 +1,15 @@ package com.unifest.android.feature.menu import android.content.pm.PackageManager +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 import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import com.unifest.android.core.designsystem.theme.UnifestTheme -import com.unifest.android.core.domain.entity.MenuEntity -import com.unifest.android.core.ui.DevicePreview -import kotlinx.collections.immutable.persistentListOf import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -25,16 +24,13 @@ 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.Text import androidx.compose.material3.TextButton import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -44,6 +40,7 @@ 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.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -57,57 +54,65 @@ 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.Title2 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.domain.entity.BoothDetailEntity import com.unifest.android.core.domain.entity.Festival +import com.unifest.android.core.ui.DevicePreview +import com.unifest.android.core.ui.component.EmptyLikedBoothItem import com.unifest.android.core.ui.component.FestivalSearchBottomSheet +import com.unifest.android.core.ui.component.LikedBoothItem 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 @Composable internal fun MenuRoute( padding: PaddingValues, - onNavigateToInterestedBooths: () -> Unit, + onNavigateToLikedBooth: () -> Unit, viewModel: MenuViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current + val appVersion = remember { + try { + context.packageManager.getPackageInfo(context.packageName, 0).versionName + } catch (e: PackageManager.NameNotFoundException) { + Timber.tag("AppVersion").e(e, "Failed to get package info") + "Unknown" + } + } MenuScreen( padding = padding, uiState = uiState, setFestivalSearchBottomSheetVisible = viewModel::setFestivalSearchBottomSheetVisible, - onNavigateToInterestedBooths = onNavigateToInterestedBooths, + onNavigateToLikedBoothList = onNavigateToLikedBooth, updateFestivalSearchText = viewModel::updateFestivalSearchText, initSearchText = viewModel::initSearchText, setEnableSearchMode = viewModel::setEnableSearchMode, setEnableEditMode = viewModel::setEnableEditMode, - setInterestedFestivalDeleteDialogVisible = viewModel::setInterestedFestivalDeleteDialogVisible, + setLikedFestivalDeleteDialogVisible = viewModel::setLikedFestivalDeleteDialogVisible, + deleteLikedBooth = viewModel::deleteLikedBooth, + appVersion = appVersion, ) } +@OptIn(ExperimentalFoundationApi::class) @Composable fun MenuScreen( padding: PaddingValues, uiState: MenuUiState, setFestivalSearchBottomSheetVisible: (Boolean) -> Unit, - onNavigateToInterestedBooths: () -> Unit, + onNavigateToLikedBoothList: () -> Unit, updateFestivalSearchText: (TextFieldValue) -> Unit, initSearchText: () -> Unit, setEnableSearchMode: (Boolean) -> Unit, setEnableEditMode: () -> Unit, - setInterestedFestivalDeleteDialogVisible: (Boolean) -> Unit, + setLikedFestivalDeleteDialogVisible: (Boolean) -> Unit, + deleteLikedBooth: (BoothDetailEntity) -> Unit, + appVersion: String, ) { - val context = LocalContext.current - val appVersion = remember { - try { - context.packageManager.getPackageInfo(context.packageName, 0).versionName - } catch (e: PackageManager.NameNotFoundException) { - Timber.tag("AppVersion").e(e, "Failed to get package info") - "Unknown" - } - } Box( modifier = Modifier .fillMaxSize() @@ -136,7 +141,7 @@ fun MenuScreen( .padding(top = 10.dp, start = 20.dp), ) { Text( - text = stringResource(id = R.string.menu_my_interested_festival), + text = stringResource(id = R.string.menu_my_liked_festival), style = Title3, ) TextButton( @@ -194,9 +199,14 @@ fun MenuScreen( .fillMaxWidth() .padding(start = 20.dp, top = 10.dp), ) { - Text(text = stringResource(id = R.string.menu_interested_booths)) + Text( + text = stringResource(id = R.string.menu_liked_booth), + style = Title3, + color = Color(0xFF161616), + fontWeight = FontWeight.Bold, + ) TextButton( - onClick = { onNavigateToInterestedBooths() }, + onClick = { onNavigateToLikedBoothList() }, modifier = Modifier.padding(end = 8.dp), ) { Text( @@ -207,8 +217,32 @@ fun MenuScreen( } } } - itemsIndexed(uiState.interestedBooths.take(3), key = { _, booth -> booth.id }) { index, booth -> - InterestedBoothsItems(booth, index, uiState.interestedBooths.size) + if (uiState.likedBoothList.isEmpty()) { + item { + EmptyLikedBoothItem( + modifier = Modifier + .fillMaxWidth() + .height(248.dp), + ) + } + } else { + itemsIndexed( + items = uiState.likedBoothList.take(3), + key = { _, booth -> booth.id }, + ) { index, booth -> + LikedBoothItem( + booth = booth, + index = index, + totalCount = uiState.likedBoothList.size, + deleteLikedBooth = { deleteLikedBooth(booth) }, + modifier = Modifier.animateItemPlacement( + animationSpec = tween( + durationMillis = 500, + easing = LinearOutSlowInEasing, + ), + ), + ) + } } item { VerticalDivider( @@ -220,7 +254,7 @@ fun MenuScreen( } item { MenuItem( - ImageVector.vectorResource(R.drawable.ic_inquiry), + icon = ImageVector.vectorResource(R.drawable.ic_inquiry), title = stringResource(id = R.string.menu_questions), onClick = { /* 구현 */ }, ) @@ -235,7 +269,7 @@ fun MenuScreen( } item { MenuItem( - ImageVector.vectorResource(R.drawable.ic_admin_mode), + icon = ImageVector.vectorResource(R.drawable.ic_admin_mode), title = stringResource(id = R.string.menu_admin_mode), onClick = { /* 구현 */ }, ) @@ -255,7 +289,11 @@ fun MenuScreen( .fillMaxWidth() .padding(vertical = 13.dp), ) { - Text("UniFest v$appVersion", textAlign = TextAlign.Center, color = Color(0xFFC5C5C5)) + Text( + text = "UniFest v$appVersion", + textAlign = TextAlign.Center, + color = Color(0xFFC5C5C5), + ) } } } @@ -266,14 +304,14 @@ fun MenuScreen( updateSearchText = updateFestivalSearchText, searchTextHintRes = R.string.festival_search_text_field_hint, setFestivalSearchBottomSheetVisible = setFestivalSearchBottomSheetVisible, - interestedFestivals = uiState.interestedFestivals, + likedFestivals = uiState.likedFestivals, festivalSearchResults = uiState.festivalSearchResults, initSearchText = initSearchText, setEnableSearchMode = setEnableSearchMode, isSearchMode = uiState.isSearchMode, setEnableEditMode = setEnableEditMode, - isInterestedFestivalDeleteDialogVisible = uiState.isInterestedFestivalDeleteDialogVisible, - setInterestedFestivalDeleteDialogVisible = setInterestedFestivalDeleteDialogVisible, + isLikedFestivalDeleteDialogVisible = uiState.isLikedFestivalDeleteDialogVisible, + setLikedFestivalDeleteDialogVisible = setLikedFestivalDeleteDialogVisible, isEditMode = uiState.isEditMode, ) } @@ -323,78 +361,11 @@ fun FestivalItem( } @Composable -fun InterestedBoothsItems(booth: BoothDetailEntity, index: Int, total: Int) { - var isBookmarked by remember { mutableStateOf(true) } - val bookMarkColor = if (isBookmarked) Color(0xFFF5687E) else Color(0xFF4B4B4B) - Column( - modifier = Modifier - .clickable { /* 클릭 이벤트 처리 */ } - .padding(horizontal = 20.dp), - ) { - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxSize(), - ) { - NetworkImage( - imageUrl = "https://picsum.photos/86", - modifier = Modifier - .size(86.dp) - .clip(RoundedCornerShape(16.dp)), - ) - Spacer(modifier = Modifier.width(14.dp)) - Column( - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - ) { - Text( - text = booth.name, - style = Title2, - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = booth.description, - style = Title5, - color = Color(0xFF545454), - ) - Spacer(modifier = Modifier.height(13.dp)) - Row { - Icon( - imageVector = ImageVector.vectorResource(id = R.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), - ) - } - } - Icon( - imageVector = ImageVector.vectorResource(if (isBookmarked) R.drawable.ic_bookmarked else R.drawable.ic_bookmark), - contentDescription = if (isBookmarked) "북마크됨" else "북마크하기", - tint = bookMarkColor, - modifier = Modifier - .size(24.dp) - .clickable(onClick = { isBookmarked = !isBookmarked }), - ) - } - Spacer(modifier = Modifier.height(16.dp)) - if (index < total - 1) { - HorizontalDivider( - color = Color(0xFFDFDFDF), - thickness = 1.dp, - modifier = Modifier.fillMaxWidth(), - ) - } - } -} - -@Composable -fun MenuItem(icon: ImageVector, title: String, onClick: () -> Unit) { +fun MenuItem( + icon: ImageVector, + title: String, + onClick: () -> Unit, +) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -403,9 +374,9 @@ fun MenuItem(icon: ImageVector, title: String, onClick: () -> Unit) { .padding(25.dp), ) { Icon( - icon, - contentDescription = null, - tint = Color(0xFF7A7A7C), + imageVector = icon, + contentDescription = "Menu Icon", + tint = Color.Unspecified, ) Spacer(modifier = Modifier.width(16.dp)) Text(title, style = Content8) @@ -418,13 +389,6 @@ fun MenuScreenPreview() { UnifestTheme { MenuScreen( padding = PaddingValues(0.dp), - setFestivalSearchBottomSheetVisible = { }, - updateFestivalSearchText = { }, - initSearchText = { }, - setEnableSearchMode = { }, - setEnableEditMode = { }, - setInterestedFestivalDeleteDialogVisible = { }, - onNavigateToInterestedBooths = { }, uiState = MenuUiState( festivals = persistentListOf( Festival( @@ -434,51 +398,40 @@ fun MenuScreenPreview() { imgUrl = "", ), Festival( - schoolName = "건국대", + schoolName = "서울대", festivalName = "녹색지대", festivalDate = "2021.11.11", imgUrl = "", ), ), - interestedBooths = persistentListOf( + likedBoothList = persistentListOf( BoothDetailEntity( id = 1, name = "부스 이름", - category = "음식", + category = "부스 카테고리", description = "부스 설명", warning = "주의사항", location = "부스 위치", - latitude = 0.0f, - longitude = 0.0f, - menus = listOf( - MenuEntity( - id = 1, - name = "메뉴 이름", - price = 1000, - imgUrl = "", - ), - ), ), BoothDetailEntity( id = 2, name = "부스 이름", - category = "음식", + category = "부스 카테고리", description = "부스 설명", warning = "주의사항", location = "부스 위치", - latitude = 0.0f, - longitude = 0.0f, - menus = listOf( - MenuEntity( - id = 1, - name = "메뉴 이름", - price = 1000, - imgUrl = "", - ), - ), ), ), ), + setFestivalSearchBottomSheetVisible = {}, + updateFestivalSearchText = {}, + initSearchText = {}, + setEnableSearchMode = {}, + setEnableEditMode = { }, + setLikedFestivalDeleteDialogVisible = {}, + onNavigateToLikedBoothList = {}, + deleteLikedBooth = {}, + appVersion = "1.0.0", ) } } 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 b4a908dc..04c3e113 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 @@ -15,12 +15,12 @@ fun NavController.navigateToMenu(navOptions: NavOptions) { fun NavGraphBuilder.menuNavGraph( padding: PaddingValues, - onNavigateToInterestedBooths: () -> Unit, + onNavigateToLikedBooth: () -> Unit, ) { composable(route = MENU_ROUTE) { MenuRoute( padding = padding, - onNavigateToInterestedBooths = onNavigateToInterestedBooths, + onNavigateToLikedBooth = onNavigateToLikedBooth, ) } } diff --git a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuUiState.kt b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuUiState.kt index 59b6184d..f961fe5f 100644 --- a/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuUiState.kt +++ b/feature/menu/src/main/kotlin/com/unifest/android/feature/menu/viewmodel/MenuUiState.kt @@ -8,12 +8,12 @@ import kotlinx.collections.immutable.persistentListOf data class MenuUiState( val festivals: ImmutableList = persistentListOf(), - val interestedBooths: ImmutableList = persistentListOf(), + val likedBoothList: ImmutableList = persistentListOf(), val festivalSearchText: TextFieldValue = TextFieldValue(), - val interestedFestivals: MutableList = mutableListOf(), + val likedFestivals: MutableList = mutableListOf(), val festivalSearchResults: ImmutableList = persistentListOf(), val isSearchMode: Boolean = false, val isEditMode: Boolean = false, val isFestivalSearchBottomSheetVisible: Boolean = false, - val isInterestedFestivalDeleteDialogVisible: Boolean = false, + val isLikedFestivalDeleteDialogVisible: Boolean = false, ) 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 25000722..537d5720 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 @@ -2,84 +2,118 @@ package com.unifest.android.feature.menu.viewmodel import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.unifest.android.core.data.repository.LikedBoothRepository import com.unifest.android.core.domain.entity.BoothDetailEntity -import com.unifest.android.core.domain.entity.Festival +import com.unifest.android.core.domain.mapper.toEntity +import com.unifest.android.core.domain.mapper.toResponse import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class MenuViewModel @Inject constructor() : ViewModel() { +class MenuViewModel @Inject constructor( + private val likedBoothRepository: LikedBoothRepository, +) : ViewModel() { private val _uiState = MutableStateFlow(MenuUiState()) val uiState: StateFlow = _uiState.asStateFlow() + init { - _uiState.update { currentState -> - currentState.copy( - interestedFestivals = mutableListOf( - Festival("https://picsum.photos/36", "서울대학교", "설대축제", "05.06-05.08"), - Festival("https://picsum.photos/36", "연세대학교", "연대축제", "05.06-05.08"), - Festival("https://picsum.photos/36", "고려대학교", "고대축제", "05.06-05.08"), - Festival("https://picsum.photos/36", "건국대학교", "녹색지대", "05.06-05.08"), - Festival("https://picsum.photos/36", "성균관대학교", "성대축제", "05.06-05.08"), - ), - festivalSearchResults = persistentListOf( - Festival("https://picsum.photos/36", "서울대학교", "설대축제", "05.06-05.08"), - Festival("https://picsum.photos/36", "연세대학교", "연대축제", "05.06-05.08"), - Festival("https://picsum.photos/36", "고려대학교", "고대축제", "05.06-05.08"), - Festival("https://picsum.photos/36", "건국대학교", "녹색지대", "05.06-05.08"), - Festival("https://picsum.photos/36", "성균관대학교", "성대축제", "05.06-05.08"), - ), - // 임시 데이터 - festivals = persistentListOf( - Festival("school_image_url_1", "서울대학교", "설대축제", "05.06-05.08"), - Festival("school_image_url_2", "연세대학교", "연대축제", "05.06-05.08"), - Festival("school_image_url_3", "고려대학교", "고대축제", "05.06-05.08"), - Festival("school_image_url_4", "건국대학교", "녹색지대", "05.06-05.08"), - Festival("school_image_url_5", "성균관대", "성대축제", "05.06-05.08"), - ), - interestedBooths = persistentListOf( - BoothDetailEntity( - id = 1, - name = "부스1", - category = "카페", - description = "부스1 설명", - warning = "부스1 주의사항", - location = "부스1 위치", - latitude = 37.5665f, - longitude = 126.9780f, - menus = listOf(), - ), - BoothDetailEntity( - id = 2, - name = "부스2", - category = "카페", - description = "부스2 설명", - warning = "부스2 주의사항", - location = "부스2 위치", - latitude = 37.5665f, - longitude = 126.9780f, - menus = listOf(), - ), - BoothDetailEntity( - id = 3, - name = "부스3", - category = "카페", - description = "부스3 설명", - warning = "부스3 주의사항", - location = "부스3 위치", - latitude = 37.5665f, - longitude = 126.9780f, - menus = listOf(), - ), - ), - ) + viewModelScope.launch { + likedBoothRepository.getLikedBoothList().collect { likedBoothList -> + _uiState.update { + it.copy( + likedBoothList = likedBoothList.map { likedBooth -> + likedBooth.toEntity() + }.toImmutableList(), + ) + } + } + } + } + + fun deleteLikedBooth(booth: BoothDetailEntity) { + viewModelScope.launch { + updateLikedBooth(booth) + delay(500) + likedBoothRepository.deleteLikedBooth(booth.toResponse()) } } + private suspend fun updateLikedBooth(booth: BoothDetailEntity) { + likedBoothRepository.updateLikedBooth(booth.copy(isLiked = false).toResponse()) + } + +// init { +// _uiState.update { currentState -> +// currentState.copy( +// likedFestivals = mutableListOf( +// Festival("https://picsum.photos/36", "서울대학교", "설대축제", "05.06-05.08"), +// Festival("https://picsum.photos/36", "연세대학교", "연대축제", "05.06-05.08"), +// Festival("https://picsum.photos/36", "고려대학교", "고대축제", "05.06-05.08"), +// Festival("https://picsum.photos/36", "건국대학교", "녹색지대", "05.06-05.08"), +// Festival("https://picsum.photos/36", "성균관대학교", "성대축제", "05.06-05.08"), +// ), +// festivalSearchResults = persistentListOf( +// Festival("https://picsum.photos/36", "서울대학교", "설대축제", "05.06-05.08"), +// Festival("https://picsum.photos/36", "연세대학교", "연대축제", "05.06-05.08"), +// Festival("https://picsum.photos/36", "고려대학교", "고대축제", "05.06-05.08"), +// Festival("https://picsum.photos/36", "건국대학교", "녹색지대", "05.06-05.08"), +// Festival("https://picsum.photos/36", "성균관대학교", "성대축제", "05.06-05.08"), +// ), +// // 임시 데이터 +// festivals = persistentListOf( +// Festival("school_image_url_1", "서울대학교", "설대축제", "05.06-05.08"), +// Festival("school_image_url_2", "연세대학교", "연대축제", "05.06-05.08"), +// Festival("school_image_url_3", "고려대학교", "고대축제", "05.06-05.08"), +// Festival("school_image_url_4", "건국대학교", "녹색지대", "05.06-05.08"), +// Festival("school_image_url_5", "성균관대", "성대축제", "05.06-05.08"), +// ), +// likedBoothList = persistentListOf( +// BoothDetailEntity( +// id = 1, +// name = "부스1", +// category = "카페", +// description = "부스1 설명", +// warning = "부스1 주의사항", +// location = "부스1 위치", +// latitude = 37.5665f, +// longitude = 126.9780f, +// menus = listOf(), +// ), +// BoothDetailEntity( +// id = 2, +// name = "부스2", +// category = "카페", +// description = "부스2 설명", +// warning = "부스2 주의사항", +// location = "부스2 위치", +// latitude = 37.5665f, +// longitude = 126.9780f, +// menus = listOf(), +// ), +// BoothDetailEntity( +// id = 3, +// name = "부스3", +// category = "카페", +// description = "부스3 설명", +// warning = "부스3 주의사항", +// location = "부스3 위치", +// latitude = 37.5665f, +// longitude = 126.9780f, +// menus = listOf(), +// ), +// ), +// ) +// } +// } + fun updateFestivalSearchText(text: TextFieldValue) { _uiState.update { it.copy(festivalSearchText = text) @@ -110,9 +144,9 @@ class MenuViewModel @Inject constructor() : ViewModel() { } } - fun setInterestedFestivalDeleteDialogVisible(flag: Boolean) { + fun setLikedFestivalDeleteDialogVisible(flag: Boolean) { _uiState.update { - it.copy(isInterestedFestivalDeleteDialogVisible = flag) + it.copy(isLikedFestivalDeleteDialogVisible = flag) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 61e97073..5cf0a0b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,6 @@ gradle-dependency-handler-extensions = "1.1.0" google-secrets = "2.0.1" google-service = "4.4.1" -play-services-maps = "18.2.0" -play-services-location = "21.2.0" kotlin-detekt = "1.23.6" kotlin-ktlint-gradle = "11.6.1" @@ -20,11 +18,12 @@ kotlinx-serialization-converter = "1.0.0" kotlinx-collections-immutable = "0.3.7" androidx-core = "1.12.0" -androidx-lifecycle = "2.8.0-alpha04" +androidx-lifecycle = "2.8.0-alpha02" androidx-splash = "1.0.1" androidx-startup = "1.1.1" androidx-navigation = "2.7.7" androidx-datastore = "1.0.0" +androidx-room = "2.6.1" androidx-activity-compose = "1.8.2" androidx-compose-compiler = "1.5.11" androidx-compose-bom = "2024.04.00" @@ -54,16 +53,12 @@ naver-map-sdk = "3.18.0" naver-map-compose = "1.5.5" naver-map-location = "21.0.1" tedclustering-naver = "1.0.2" -junit = "4.13.2" -androidx-test-ext-junit = "1.1.5" -espresso-core = "3.5.1" -appcompat = "1.6.1" -material = "1.11.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" } 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,6 +88,10 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 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" } +androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "androidx-room" } +androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" } + compose-system-ui-controller = { group = "tech.thdev", name = "extensions-compose-system-ui-controller", version.ref = "compose-extensions" } compose-keyboard-state = { group = "tech.thdev", name = "extensions-compose-keyboard-state", version.ref = "compose-extensions" } @@ -120,11 +119,6 @@ naver-map-sdk = { group = "com.naver.maps", name = "map-sdk", version.ref = "nav 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-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } -espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } -appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } [plugins] @@ -135,7 +129,6 @@ google-service = { id = "com.google.gms.google-services", version.ref = "google- kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-core" } kotlin-detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "kotlin-detekt" } -kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-core" } kotlin-ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "kotlin-ktlint-gradle" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin-core" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-core" } @@ -143,6 +136,8 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } +androidx-room = { id = "androidx.room", version.ref = "androidx-room" } + hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } @@ -157,6 +152,7 @@ unifest-android-library-compose = { id = "unifest.android.library.compose", vers unifest-android-feature = { id = "unifest.android.feature", version = "unspecified" } unifest-android-hilt = { id = "unifest.android.hilt", version = "unspecified" } unifest-android-retrofit = { id = "unifest.android.retrofit", version = "unspecified" } +unifest-android-room = { id = "unifest.android.room", version = "unspecified" } [bundles] diff --git a/settings.gradle.kts b/settings.gradle.kts index 641e52c3..2df1424d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,16 +27,16 @@ include( ":core:common", ":core:data", - ":core:designsystem", + ":core:database", ":core:datastore", + ":core:designsystem", ":core:domain", ":core:network", - ":core:datastore", ":core:ui", ":feature:booth", ":feature:home", - ":feature:interested-booth", + ":feature:liked-booth", ":feature:intro", ":feature:main", ":feature:map",