diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/EmotionRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/EmotionRepository.kt new file mode 100644 index 000000000..3bedf4ed9 --- /dev/null +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/EmotionRepository.kt @@ -0,0 +1,7 @@ +package com.ninecraft.booket.core.data.api.repository + +import com.ninecraft.booket.core.model.EmotionGroupsModel + +interface EmotionRepository { + suspend fun getEmotions(): Result +} diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/RecordRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/RecordRepository.kt index 753bc76bc..2c13ec334 100644 --- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/RecordRepository.kt +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/RecordRepository.kt @@ -1,37 +1,37 @@ package com.ninecraft.booket.core.data.api.repository -import com.ninecraft.booket.core.model.ReadingRecordModel -import com.ninecraft.booket.core.model.RecordRegisterModel +import com.ninecraft.booket.core.model.ReadingRecordModelV2 import com.ninecraft.booket.core.model.ReadingRecordsModel -import com.ninecraft.booket.core.model.RecordDetailModel interface RecordRepository { suspend fun postRecord( userBookId: String, - pageNumber: Int, + pageNumber: Int?, quote: String, - emotionTags: List, review: String, - ): Result + primaryEmotion: String, + detailEmotionTagIds: List, + ): Result suspend fun getReadingRecords( userBookId: String, sort: String, page: Int, size: Int, - ): Result + ): Result // TODO: V2로 변경 필요 suspend fun getRecordDetail( readingRecordId: String, - ): Result + ): Result suspend fun editRecord( readingRecordId: String, - pageNumber: Int, + pageNumber: Int?, quote: String, - emotionTags: List, review: String, - ): Result + primaryEmotion: String, + detailEmotionTagIds: List, + ): Result suspend fun deleteRecord( readingRecordId: String, diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/DataGraph.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/DataGraph.kt index 8dc2680cb..9190ef5df 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/DataGraph.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/di/DataGraph.kt @@ -2,11 +2,13 @@ package com.ninecraft.booket.core.data.impl.di import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.BookRepository +import com.ninecraft.booket.core.data.api.repository.EmotionRepository import com.ninecraft.booket.core.data.api.repository.RecordRepository import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository import com.ninecraft.booket.core.data.api.repository.UserRepository import com.ninecraft.booket.core.data.impl.repository.DefaultAuthRepository import com.ninecraft.booket.core.data.impl.repository.DefaultBookRepository +import com.ninecraft.booket.core.data.impl.repository.DefaultEmotionRepository import com.ninecraft.booket.core.data.impl.repository.DefaultRecordRepository import com.ninecraft.booket.core.data.impl.repository.DefaultRemoteConfigRepository import com.ninecraft.booket.core.data.impl.repository.DefaultUserRepository @@ -23,6 +25,9 @@ interface DataGraph { @Binds val DefaultBookRepository.bind: BookRepository + @Binds + val DefaultEmotionRepository.bind: EmotionRepository + @Binds val DefaultRecordRepository.bind: RecordRepository diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt index 66a1c8933..822eb87ea 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/mapper/ResponseToModel.kt @@ -1,22 +1,26 @@ package com.ninecraft.booket.core.data.impl.mapper import com.ninecraft.booket.core.common.extensions.decodeHtmlEntities -import com.ninecraft.booket.core.common.extensions.toFormattedDate import com.ninecraft.booket.core.model.BookDetailModel import com.ninecraft.booket.core.model.BookSearchModel import com.ninecraft.booket.core.model.BookSummaryModel import com.ninecraft.booket.core.model.BookUpsertModel +import com.ninecraft.booket.core.model.DetailEmotionModel import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.EmotionGroupModel +import com.ninecraft.booket.core.model.EmotionGroupsModel import com.ninecraft.booket.core.model.EmotionModel import com.ninecraft.booket.core.model.HomeModel import com.ninecraft.booket.core.model.LibraryBookSummaryModel import com.ninecraft.booket.core.model.LibraryBooksModel import com.ninecraft.booket.core.model.LibraryModel import com.ninecraft.booket.core.model.PageInfoModel +import com.ninecraft.booket.core.model.PrimaryEmotionModel import com.ninecraft.booket.core.model.ReadingRecordModel +import com.ninecraft.booket.core.model.ReadingRecordModelV2 import com.ninecraft.booket.core.model.ReadingRecordsModel import com.ninecraft.booket.core.model.RecentBookModel -import com.ninecraft.booket.core.model.RecordDetailModel import com.ninecraft.booket.core.model.RecordRegisterModel import com.ninecraft.booket.core.model.SeedModel import com.ninecraft.booket.core.model.TermsAgreementModel @@ -26,6 +30,9 @@ import com.ninecraft.booket.core.network.response.BookSearchResponse import com.ninecraft.booket.core.network.response.BookSummary import com.ninecraft.booket.core.network.response.BookUpsertResponse import com.ninecraft.booket.core.network.response.Category +import com.ninecraft.booket.core.network.response.DetailEmotion +import com.ninecraft.booket.core.network.response.EmotionGroup +import com.ninecraft.booket.core.network.response.EmotionGroupsResponse import com.ninecraft.booket.core.network.response.GuestBookSearchResponse import com.ninecraft.booket.core.network.response.GuestBookSummary import com.ninecraft.booket.core.network.response.HomeResponse @@ -33,10 +40,11 @@ import com.ninecraft.booket.core.network.response.LibraryBookSummary import com.ninecraft.booket.core.network.response.LibraryBooks import com.ninecraft.booket.core.network.response.LibraryResponse import com.ninecraft.booket.core.network.response.PageInfo +import com.ninecraft.booket.core.network.response.PrimaryEmotion import com.ninecraft.booket.core.network.response.ReadingRecord +import com.ninecraft.booket.core.network.response.ReadingRecordV2 import com.ninecraft.booket.core.network.response.ReadingRecordsResponse import com.ninecraft.booket.core.network.response.RecentBook -import com.ninecraft.booket.core.network.response.RecordDetailResponse import com.ninecraft.booket.core.network.response.RecordRegisterResponse import com.ninecraft.booket.core.network.response.SeedResponse import com.ninecraft.booket.core.network.response.TermsAgreementResponse @@ -187,6 +195,28 @@ internal fun PageInfo.toModel(): PageInfoModel { ) } +internal fun EmotionGroupsResponse.toModel(): EmotionGroupsModel { + return EmotionGroupsModel( + emotions = emotions.map { it.toModel() }, + ) +} + +internal fun EmotionGroup.toModel(): EmotionGroupModel { + val code = EmotionCode.fromCode(code) ?: EmotionCode.OTHER + return EmotionGroupModel( + code = code, + displayName = displayName, + detailEmotions = detailEmotions.map { it.toModel() }, + ) +} + +internal fun DetailEmotion.toModel(): DetailEmotionModel { + return DetailEmotionModel( + id = id, + name = name, + ) +} + internal fun RecordRegisterResponse.toModel(): RecordRegisterModel { return RecordRegisterModel( id = id, @@ -220,27 +250,36 @@ internal fun ReadingRecord.toModel(): ReadingRecordModel { emotionTags = emotionTags, createdAt = createdAt, updatedAt = updatedAt, - bookTitle = bookTitle, - bookPublisher = bookPublisher, - bookCoverImageUrl = bookCoverImageUrl, - author = author, + bookTitle = bookTitle ?: "", + bookPublisher = bookPublisher ?: "", + bookCoverImageUrl = bookCoverImageUrl ?: "", + author = author ?: "", ) } -internal fun RecordDetailResponse.toModel(): RecordDetailModel { - return RecordDetailModel( +internal fun ReadingRecordV2.toModel(): ReadingRecordModelV2 { + return ReadingRecordModelV2( id = id, userBookId = userBookId, pageNumber = pageNumber, quote = quote, review = review ?: "", - emotionTags = emotionTags, - createdAt = createdAt.toFormattedDate(), - updatedAt = updatedAt.toFormattedDate(), - bookTitle = bookTitle, - bookPublisher = bookPublisher, - bookCoverImageUrl = bookCoverImageUrl, - author = author, + primaryEmotion = primaryEmotion.toModel(), + detailEmotions = detailEmotions.map { it.toModel() }, + createdAt = createdAt, + updatedAt = updatedAt, + bookTitle = bookTitle ?: "", + bookPublisher = bookPublisher ?: "", + bookCoverImageUrl = bookCoverImageUrl ?: "", + author = author ?: "", + ) +} + +internal fun PrimaryEmotion.toModel(): PrimaryEmotionModel { + val code = EmotionCode.fromCode(code) ?: EmotionCode.OTHER + return PrimaryEmotionModel( + code = code, + displayName = displayName, ) } diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultEmotionRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultEmotionRepository.kt new file mode 100644 index 000000000..95ec3b588 --- /dev/null +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultEmotionRepository.kt @@ -0,0 +1,20 @@ +package com.ninecraft.booket.core.data.impl.repository + +import com.ninecraft.booket.core.common.utils.runSuspendCatching +import com.ninecraft.booket.core.data.api.repository.EmotionRepository +import com.ninecraft.booket.core.data.impl.mapper.toModel +import com.ninecraft.booket.core.di.DataScope +import com.ninecraft.booket.core.model.EmotionGroupsModel +import com.ninecraft.booket.core.network.service.ReedService +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn + +@SingleIn(DataScope::class) +@Inject +class DefaultEmotionRepository( + private val service: ReedService, +) : EmotionRepository { + override suspend fun getEmotions(): Result = runSuspendCatching { + service.getEmotions().toModel() + } +} diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRecordRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRecordRepository.kt index 966a40db7..776a4efe1 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRecordRepository.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultRecordRepository.kt @@ -3,11 +3,10 @@ package com.ninecraft.booket.core.data.impl.repository import com.ninecraft.booket.core.common.utils.runSuspendCatching import com.ninecraft.booket.core.data.api.repository.RecordRepository import com.ninecraft.booket.core.data.impl.mapper.toModel -import com.ninecraft.booket.core.model.ReadingRecordModel +import com.ninecraft.booket.core.di.DataScope import com.ninecraft.booket.core.network.request.RecordRegisterRequest import com.ninecraft.booket.core.network.service.ReedService import dev.zacsweers.metro.Inject -import com.ninecraft.booket.core.di.DataScope import dev.zacsweers.metro.SingleIn @SingleIn(DataScope::class) @@ -17,12 +16,13 @@ class DefaultRecordRepository( ) : RecordRepository { override suspend fun postRecord( userBookId: String, - pageNumber: Int, + pageNumber: Int?, quote: String, - emotionTags: List, review: String, + primaryEmotion: String, + detailEmotionTagIds: List, ) = runSuspendCatching { - service.postRecord(userBookId, RecordRegisterRequest(pageNumber, quote, emotionTags, review)).toModel() + service.postRecord(userBookId, RecordRegisterRequest(pageNumber, quote, review, primaryEmotion, detailEmotionTagIds)).toModel() } override suspend fun getReadingRecords( @@ -40,12 +40,13 @@ class DefaultRecordRepository( override suspend fun editRecord( readingRecordId: String, - pageNumber: Int, + pageNumber: Int?, quote: String, - emotionTags: List, review: String, - ): Result = runSuspendCatching { - service.editRecord(readingRecordId, RecordRegisterRequest(pageNumber, quote, emotionTags, review)).toModel() + primaryEmotion: String, + detailEmotionTagIds: List, + ) = runSuspendCatching { + service.editRecord(readingRecordId, RecordRegisterRequest(pageNumber, quote, review, primaryEmotion, detailEmotionTagIds)).toModel() } override suspend fun deleteRecord(readingRecordId: String): Result = runSuspendCatching { diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt index 52bb4fdb0..ef46f7c1c 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/Emotion.kt @@ -17,6 +17,7 @@ import com.ninecraft.booket.core.designsystem.theme.WarmthBgColor import com.ninecraft.booket.core.designsystem.theme.WarmthTextColor import com.ninecraft.booket.core.designsystem.theme.Yellow300 import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.core.model.EmotionCode val Emotion.bgColor: Color get() = when (this) { @@ -36,15 +37,6 @@ val Emotion.textColor: Color Emotion.ETC -> EtcTextColor } -val Emotion.graphicRes: Int - get() = when (this) { - Emotion.WARM -> R.drawable.img_emotion_warmth - Emotion.JOY -> R.drawable.img_emotion_joy - Emotion.SAD -> R.drawable.img_emotion_sadness - Emotion.INSIGHT -> R.drawable.img_emotion_insight - Emotion.ETC -> R.drawable.img_emotion_warmth - } - val Emotion.ratioBarColor: Color get() = when (this) { Emotion.WARM -> Yellow300 @@ -54,20 +46,29 @@ val Emotion.ratioBarColor: Color Emotion.ETC -> Neutral300 } -val Emotion.graphicResV2: Int? +val EmotionCode.graphicRes: Int + get() = when (this) { + EmotionCode.WARMTH -> R.drawable.img_warmth + EmotionCode.JOY -> R.drawable.img_joy + EmotionCode.SADNESS -> R.drawable.img_sadness + EmotionCode.INSIGHT -> R.drawable.img_insight + EmotionCode.OTHER -> R.drawable.img_other + } + +val EmotionCode.categoryGraphicRes: Int? get() = when (this) { - Emotion.WARM -> R.drawable.img_category_warm - Emotion.JOY -> R.drawable.img_category_joy - Emotion.SAD -> R.drawable.img_category_sad - Emotion.INSIGHT -> R.drawable.img_category_insight - Emotion.ETC -> null + EmotionCode.WARMTH -> R.drawable.img_category_warmth + EmotionCode.JOY -> R.drawable.img_category_joy + EmotionCode.SADNESS -> R.drawable.img_category_sadness + EmotionCode.INSIGHT -> R.drawable.img_category_insight + EmotionCode.OTHER -> null } -val Emotion.descriptionRes: Int +val EmotionCode.descriptionRes: Int get() = when (this) { - Emotion.WARM -> R.string.emotion_warm_description - Emotion.JOY -> R.string.emotion_joy_description - Emotion.SAD -> R.string.emotion_sad_description - Emotion.INSIGHT -> R.string.emotion_insight_description - Emotion.ETC -> R.string.emotion_etc_description + EmotionCode.WARMTH -> R.string.emotion_warm_description + EmotionCode.JOY -> R.string.emotion_joy_description + EmotionCode.SADNESS -> R.string.emotion_sad_description + EmotionCode.INSIGHT -> R.string.emotion_insight_description + EmotionCode.OTHER -> R.string.emotion_other_description } diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/RecordStep.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/RecordStep.kt index 1e5eb2eb5..8c79ae8fd 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/RecordStep.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/RecordStep.kt @@ -3,7 +3,6 @@ package com.ninecraft.booket.core.designsystem enum class RecordStep { QUOTE, EMOTION, - IMPRESSION, ; val value: Int get() = ordinal diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/RecordProgressBar.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/RecordProgressBar.kt index 4fd65bf4d..53302b7c5 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/RecordProgressBar.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/RecordProgressBar.kt @@ -23,7 +23,7 @@ fun RecordProgressBar( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing1), ) { - repeat(3) { index -> + repeat(2) { index -> val bgColor = if (index <= currentStep.ordinal) ReedTheme.colors.bgPrimary else ReedTheme.colors.bgDisabled Box( diff --git a/core/designsystem/src/main/res/drawable/img_category_sad.webp b/core/designsystem/src/main/res/drawable/img_category_sadness.webp similarity index 100% rename from core/designsystem/src/main/res/drawable/img_category_sad.webp rename to core/designsystem/src/main/res/drawable/img_category_sadness.webp diff --git a/core/designsystem/src/main/res/drawable/img_category_warm.webp b/core/designsystem/src/main/res/drawable/img_category_warmth.webp similarity index 100% rename from core/designsystem/src/main/res/drawable/img_category_warm.webp rename to core/designsystem/src/main/res/drawable/img_category_warmth.webp diff --git a/core/designsystem/src/main/res/drawable/img_emotion_insight.webp b/core/designsystem/src/main/res/drawable/img_emotion_insight.webp deleted file mode 100644 index c29fecdc7..000000000 Binary files a/core/designsystem/src/main/res/drawable/img_emotion_insight.webp and /dev/null differ diff --git a/core/designsystem/src/main/res/drawable/img_emotion_joy.webp b/core/designsystem/src/main/res/drawable/img_emotion_joy.webp deleted file mode 100644 index b2e9699bb..000000000 Binary files a/core/designsystem/src/main/res/drawable/img_emotion_joy.webp and /dev/null differ diff --git a/core/designsystem/src/main/res/drawable/img_emotion_sadness.webp b/core/designsystem/src/main/res/drawable/img_emotion_sadness.webp deleted file mode 100644 index 0af80f209..000000000 Binary files a/core/designsystem/src/main/res/drawable/img_emotion_sadness.webp and /dev/null differ diff --git a/core/designsystem/src/main/res/drawable/img_emotion_warmth.webp b/core/designsystem/src/main/res/drawable/img_emotion_warmth.webp deleted file mode 100644 index 58fc62f69..000000000 Binary files a/core/designsystem/src/main/res/drawable/img_emotion_warmth.webp and /dev/null differ diff --git a/core/designsystem/src/main/res/drawable/img_insight.webp b/core/designsystem/src/main/res/drawable/img_insight.webp new file mode 100644 index 000000000..6a1d7c903 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_insight.webp differ diff --git a/core/designsystem/src/main/res/drawable/img_joy.webp b/core/designsystem/src/main/res/drawable/img_joy.webp new file mode 100644 index 000000000..4ef8c6b1c Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_joy.webp differ diff --git a/core/designsystem/src/main/res/drawable/img_other.webp b/core/designsystem/src/main/res/drawable/img_other.webp new file mode 100644 index 000000000..239de1791 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_other.webp differ diff --git a/core/designsystem/src/main/res/drawable/img_sadness.webp b/core/designsystem/src/main/res/drawable/img_sadness.webp new file mode 100644 index 000000000..47dbb5545 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_sadness.webp differ diff --git a/core/designsystem/src/main/res/drawable/img_warmth.webp b/core/designsystem/src/main/res/drawable/img_warmth.webp new file mode 100644 index 000000000..af7344207 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/img_warmth.webp differ diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 1bc404a78..57f884365 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -8,5 +8,5 @@ 흥미롭고 유쾌한 순간 눈물이 고인 순간 생각이 깊어지는 순간 - 네 가지 감정으로 표현하기 어려울 때 + 네 가지 감정으로 표현하기 어려울 때 diff --git a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/EmotionModel.kt b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/EmotionModel.kt new file mode 100644 index 000000000..762852e34 --- /dev/null +++ b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/EmotionModel.kt @@ -0,0 +1,31 @@ +package com.ninecraft.booket.core.model + +import androidx.compose.runtime.Stable + +@Stable +data class EmotionGroupsModel( + val emotions: List, +) + +@Stable +data class EmotionGroupModel( + val code: EmotionCode, + val displayName: String, + val detailEmotions: List, +) + +@Stable +data class DetailEmotionModel( + val id: String, + val name: String, +) + +enum class EmotionCode { + WARMTH, JOY, SADNESS, INSIGHT, OTHER; + + companion object { + fun fromCode(code: String): EmotionCode? { + return EmotionCode.entries.find { it.name == code } + } + } +} diff --git a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/ReadingRecordsModel.kt b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/ReadingRecordsModel.kt index 6bf289d17..8d6f71408 100644 --- a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/ReadingRecordsModel.kt +++ b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/ReadingRecordsModel.kt @@ -1,6 +1,7 @@ package com.ninecraft.booket.core.model import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable data class ReadingRecordsModel( val lastPage: Boolean = true, @@ -25,3 +26,26 @@ data class ReadingRecordModel( val bookCoverImageUrl: String = "", val author: String = "", ) + +@Stable +data class ReadingRecordModelV2( + val id: String = "", + val userBookId: String = "", + val pageNumber: Int? = null, + val quote: String = "", + val review: String = "", + val primaryEmotion: PrimaryEmotionModel = PrimaryEmotionModel(), + val detailEmotions: List = emptyList(), + val createdAt: String = "", + val updatedAt: String = "", + val bookTitle: String = "", + val bookPublisher: String = "", + val bookCoverImageUrl: String = "", + val author: String = "", +) + +@Stable +data class PrimaryEmotionModel( + val code: EmotionCode = EmotionCode.OTHER, + val displayName: String = "기타", +) diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/RecordRegisterRequest.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/RecordRegisterRequest.kt index e26f533d9..a09aeccf1 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/RecordRegisterRequest.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/request/RecordRegisterRequest.kt @@ -6,11 +6,13 @@ import kotlinx.serialization.Serializable @Serializable data class RecordRegisterRequest( @SerialName("pageNumber") - val pageNumber: Int, + val pageNumber: Int?, @SerialName("quote") val quote: String, - @SerialName("emotionTags") - val emotionTags: List, @SerialName("review") val review: String, + @SerialName("primaryEmotion") + val primaryEmotion: String, + @SerialName("detailEmotionTagIds") + val detailEmotionTagIds: List, ) diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/EmotionGroupsResponse.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/EmotionGroupsResponse.kt new file mode 100644 index 000000000..091ab1e33 --- /dev/null +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/EmotionGroupsResponse.kt @@ -0,0 +1,28 @@ +package com.ninecraft.booket.core.network.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EmotionGroupsResponse( + @SerialName("emotions") + val emotions: List, +) + +@Serializable +data class EmotionGroup( + @SerialName("code") + val code: String, + @SerialName("displayName") + val displayName: String, + @SerialName("detailEmotions") + val detailEmotions: List, +) + +@Serializable +data class DetailEmotion( + @SerialName("id") + val id: String, + @SerialName("name") + val name: String, +) diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/ReadingRecordsResponse.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/ReadingRecordsResponse.kt index f11488193..d17c3c904 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/ReadingRecordsResponse.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/ReadingRecordsResponse.kt @@ -30,17 +30,55 @@ data class ReadingRecord( @SerialName("review") val review: String?, @SerialName("emotionTags") - val emotionTags: List = emptyList(), + val emotionTags: List, @SerialName("createdAt") val createdAt: String, @SerialName("updatedAt") val updatedAt: String, @SerialName("bookTitle") - val bookTitle: String, + val bookTitle: String?, @SerialName("bookPublisher") - val bookPublisher: String, + val bookPublisher: String?, @SerialName("bookCoverImageUrl") - val bookCoverImageUrl: String, + val bookCoverImageUrl: String?, @SerialName("author") - val author: String, + val author: String?, +) + +@Serializable +data class ReadingRecordV2( + @SerialName("id") + val id: String, + @SerialName("userBookId") + val userBookId: String, + @SerialName("pageNumber") + val pageNumber: Int?, + @SerialName("quote") + val quote: String, + @SerialName("review") + val review: String?, + @SerialName("primaryEmotion") + val primaryEmotion: PrimaryEmotion, + @SerialName("detailEmotions") + val detailEmotions: List, + @SerialName("createdAt") + val createdAt: String, + @SerialName("updatedAt") + val updatedAt: String, + @SerialName("bookTitle") + val bookTitle: String?, + @SerialName("bookPublisher") + val bookPublisher: String?, + @SerialName("bookCoverImageUrl") + val bookCoverImageUrl: String?, + @SerialName("author") + val author: String?, +) + +@Serializable +data class PrimaryEmotion( + @SerialName("code") + val code: String, + @SerialName("displayName") + val displayName: String, ) diff --git a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt index 0521ac594..8b8e6261a 100644 --- a/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt +++ b/core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt @@ -10,14 +10,13 @@ import com.ninecraft.booket.core.network.request.TermsAgreementRequest import com.ninecraft.booket.core.network.response.BookDetailResponse import com.ninecraft.booket.core.network.response.BookSearchResponse import com.ninecraft.booket.core.network.response.BookUpsertResponse +import com.ninecraft.booket.core.network.response.EmotionGroupsResponse import com.ninecraft.booket.core.network.response.GuestBookSearchResponse import com.ninecraft.booket.core.network.response.HomeResponse import com.ninecraft.booket.core.network.response.LibraryResponse import com.ninecraft.booket.core.network.response.LoginResponse -import com.ninecraft.booket.core.network.response.ReadingRecord +import com.ninecraft.booket.core.network.response.ReadingRecordV2 import com.ninecraft.booket.core.network.response.ReadingRecordsResponse -import com.ninecraft.booket.core.network.response.RecordDetailResponse -import com.ninecraft.booket.core.network.response.RecordRegisterResponse import com.ninecraft.booket.core.network.response.RefreshTokenResponse import com.ninecraft.booket.core.network.response.SeedResponse import com.ninecraft.booket.core.network.response.TermsAgreementResponse @@ -25,7 +24,6 @@ import com.ninecraft.booket.core.network.response.UserProfileResponse import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET -import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Path @@ -107,12 +105,16 @@ interface ReedService { @Path("userBookId") userBookId: String, ) + // Emotions (auth required) + @GET("api/v2/emotions") + suspend fun getEmotions(): EmotionGroupsResponse + // Reading-records endpoints (auth required) - @POST("api/v1/reading-records/{userBookId}") + @POST("api/v2/reading-records/{userBookId}") suspend fun postRecord( @Path("userBookId") userBookId: String, @Body recordRegisterRequest: RecordRegisterRequest, - ): RecordRegisterResponse + ): ReadingRecordV2 @GET("api/v1/reading-records/{userBookId}") suspend fun getReadingRecords( @@ -130,15 +132,15 @@ interface ReedService { @GET("api/v2/reading-records/detail/{readingRecordId}") suspend fun getRecordDetail( @Path("readingRecordId") readingRecordId: String, - ): RecordDetailResponse + ): ReadingRecordV2 - @PATCH("api/v1/reading-records/{readingRecordId}") + @PUT("api/v2/reading-records/{readingRecordId}") suspend fun editRecord( @Path("readingRecordId") readingRecordId: String, @Body recordRegisterRequest: RecordRegisterRequest, - ): ReadingRecord + ): ReadingRecordV2 - @DELETE("api/v1/reading-records/{readingRecordId}") + @DELETE("api/v2/reading-records/{readingRecordId}") suspend fun deleteRecord( @Path("readingRecordId") readingRecordId: String, ) diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt index 7d9ca4422..25d44ccba 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt @@ -12,6 +12,7 @@ import com.ninecraft.booket.core.common.utils.handleException import com.ninecraft.booket.core.data.api.repository.BookRepository import com.ninecraft.booket.core.data.api.repository.RecordRepository import com.ninecraft.booket.core.model.BookDetailModel +import com.ninecraft.booket.core.model.EmotionCode import com.ninecraft.booket.core.model.EmotionModel import com.ninecraft.booket.core.model.ReadingRecordModel import com.ninecraft.booket.core.ui.component.FooterState @@ -21,6 +22,8 @@ import com.ninecraft.booket.feature.screens.RecordCardScreen import com.ninecraft.booket.feature.screens.RecordDetailScreen import com.ninecraft.booket.feature.screens.RecordEditScreen import com.ninecraft.booket.feature.screens.RecordScreen +import com.ninecraft.booket.feature.screens.arguments.DetailEmotionArg +import com.ninecraft.booket.feature.screens.arguments.PrimaryEmotionArg import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs import com.orhanobut.logger.Logger import com.skydoves.compose.effects.RememberedEffect @@ -315,7 +318,7 @@ class BookDetailPresenter( RecordCardScreen( quote = selectedRecordInfo.quote, bookTitle = selectedRecordInfo.bookTitle, - emotion = selectedRecordInfo.emotionTags[0], + emotionCode = EmotionCode.OTHER, // TODO: 고정값 임시 조치 ), ) } @@ -329,7 +332,11 @@ class BookDetailPresenter( pageNumber = selectedRecordInfo.pageNumber, quote = selectedRecordInfo.quote, review = selectedRecordInfo.review, - emotionTags = selectedRecordInfo.emotionTags, + primaryEmotion = PrimaryEmotionArg( + code = EmotionCode.OTHER, + displayName = "기타", + ), // TODO: 고정값 임시 조치 + detailEmotions = listOf(DetailEmotionArg("", "")), // TODO: 고정값 임시 조치 bookTitle = selectedRecordInfo.bookTitle, bookPublisher = selectedRecordInfo.bookPublisher, bookCoverImageUrl = selectedRecordInfo.bookCoverImageUrl, diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardPresenter.kt index c580344bb..f7c0cb30f 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardPresenter.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardPresenter.kt @@ -81,7 +81,7 @@ class RecordCardPresenter( isLoading = isLoading, quote = screen.quote, bookTitle = screen.bookTitle, - emotion = screen.emotion, + emotionCode = screen.emotionCode, isCapturing = isCapturing, isSharing = isSharing, sideEffect = sideEffect, diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUi.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUi.kt index 9c75111c1..006166d6d 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUi.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUi.kt @@ -83,7 +83,7 @@ internal fun RecordCardUi( RecordCard( quote = state.quote, bookTitle = state.bookTitle, - emotion = state.emotion, + emotionCode = state.emotionCode, modifier = Modifier .padding(top = ReedTheme.spacing.spacing5) .clip(RoundedCornerShape(ReedTheme.radius.md)) diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUiState.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUiState.kt index 43c2c71a3..9b49e8e30 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUiState.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/RecordCardUiState.kt @@ -2,6 +2,7 @@ package com.ninecraft.booket.feature.detail.card import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.ImageBitmap +import com.ninecraft.booket.core.model.EmotionCode import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import java.util.UUID @@ -11,7 +12,7 @@ data class RecordCardUiState( val quote: String = "", val bookTitle: String = "", val author: String = "", - val emotion: String = "", + val emotionCode: EmotionCode = EmotionCode.OTHER, val isCapturing: Boolean = false, val isSharing: Boolean = false, val sideEffect: RecordCardSideEffect? = null, diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/component/RecordCard.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/component/RecordCard.kt index 4c055930b..3b64a4809 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/component/RecordCard.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/card/component/RecordCard.kt @@ -20,19 +20,19 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.sp import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.core.model.EmotionCode import com.ninecraft.booket.feature.detail.R @Composable internal fun RecordCard( quote: String, bookTitle: String, - emotion: String, + emotionCode: EmotionCode, modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxWidth()) { Image( - painter = painterResource(getEmotionCardImage(emotion)), + painter = painterResource(getEmotionCardImage(emotionCode)), contentDescription = "Record Card Image", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, @@ -74,13 +74,13 @@ internal fun RecordCard( } } -private fun getEmotionCardImage(emotion: String): Int { - return when (emotion) { - Emotion.WARM.displayName -> R.drawable.img_record_card_warm - Emotion.JOY.displayName -> R.drawable.img_record_card_joy - Emotion.SAD.displayName -> R.drawable.img_record_card_sad - Emotion.INSIGHT.displayName -> R.drawable.img_record_card_insight - else -> R.drawable.img_record_card_warm +private fun getEmotionCardImage(emotionCode: EmotionCode): Int { + return when (emotionCode) { + EmotionCode.WARMTH -> R.drawable.img_record_card_warm + EmotionCode.JOY -> R.drawable.img_record_card_joy + EmotionCode.SADNESS -> R.drawable.img_record_card_sad + EmotionCode.INSIGHT -> R.drawable.img_record_card_insight + EmotionCode.OTHER -> R.drawable.img_record_card_other } } @@ -91,7 +91,7 @@ private fun RecordCardPreview() { RecordCard( quote = "이 세상에 집이라 이름 붙일 수 없는 것이 있다면 그건 바로 여기, 내가 앉아 있는 이곳일 것이다.", bookTitle = "샤이닝", - emotion = Emotion.WARM.displayName, + emotionCode = EmotionCode.WARMTH, ) } } diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt index 9f38f7a7b..1c0d31579 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt @@ -8,11 +8,13 @@ import androidx.compose.runtime.setValue import com.ninecraft.booket.core.common.analytics.AnalyticsHelper import com.ninecraft.booket.core.common.utils.handleException import com.ninecraft.booket.core.data.api.repository.RecordRepository -import com.ninecraft.booket.core.model.RecordDetailModel +import com.ninecraft.booket.core.model.ReadingRecordModelV2 import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.RecordCardScreen import com.ninecraft.booket.feature.screens.RecordDetailScreen import com.ninecraft.booket.feature.screens.RecordEditScreen +import com.ninecraft.booket.feature.screens.arguments.DetailEmotionArg +import com.ninecraft.booket.feature.screens.arguments.PrimaryEmotionArg import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs import com.orhanobut.logger.Logger import com.skydoves.compose.effects.RememberedEffect @@ -50,7 +52,7 @@ class RecordDetailPresenter( override fun present(): RecordDetailUiState { val scope = rememberCoroutineScope() var uiState by rememberRetained { mutableStateOf(UiState.Idle) } - var recordDetailInfo by rememberRetained { mutableStateOf(RecordDetailModel()) } + var recordDetailInfo by rememberRetained { mutableStateOf(ReadingRecordModelV2()) } var isRecordMenuBottomSheetVisible by rememberRetained { mutableStateOf(false) } var isRecordDeleteDialogVisible by rememberRetained { mutableStateOf(false) } var sideEffect by rememberRetained { mutableStateOf(null) } @@ -134,7 +136,7 @@ class RecordDetailPresenter( RecordCardScreen( quote = recordDetailInfo.quote, bookTitle = recordDetailInfo.bookTitle, - emotion = recordDetailInfo.emotionTags[0], + emotionCode = recordDetailInfo.primaryEmotion.code, ), ) } @@ -148,7 +150,16 @@ class RecordDetailPresenter( pageNumber = recordDetailInfo.pageNumber, quote = recordDetailInfo.quote, review = recordDetailInfo.review, - emotionTags = recordDetailInfo.emotionTags, + primaryEmotion = PrimaryEmotionArg( + code = recordDetailInfo.primaryEmotion.code, + displayName = recordDetailInfo.primaryEmotion.displayName, + ), + detailEmotions = recordDetailInfo.detailEmotions.map { + DetailEmotionArg( + id = it.id, + name = it.name, + ) + }, bookTitle = recordDetailInfo.bookTitle, bookPublisher = recordDetailInfo.bookPublisher, bookCoverImageUrl = recordDetailInfo.bookCoverImageUrl, diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt index 3f3de8067..e73ba02a5 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt @@ -19,7 +19,7 @@ import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.component.ReedDivider import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White -import com.ninecraft.booket.core.model.RecordDetailModel +import com.ninecraft.booket.core.model.ReadingRecordModelV2 import com.ninecraft.booket.core.ui.ReedScaffold import com.ninecraft.booket.core.ui.component.ReedDialog import com.ninecraft.booket.core.ui.component.ReedErrorUi @@ -34,6 +34,7 @@ import com.ninecraft.booket.feature.screens.RecordDetailScreen import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dev.zacsweers.metro.AppScope +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch import com.ninecraft.booket.core.designsystem.R as designR @@ -163,7 +164,8 @@ private fun RecordDetailContent( ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) ReviewItem( - emotion = state.recordDetailInfo.emotionTags.getOrNull(0) ?: "", + primaryEmotion = state.recordDetailInfo.primaryEmotion, + detailEmotions = state.recordDetailInfo.detailEmotions.toPersistentList(), createdAt = state.recordDetailInfo.createdAt, review = state.recordDetailInfo.review, ) @@ -187,13 +189,12 @@ private fun ReviewDetailPreview() { RecordDetailUi( state = RecordDetailUiState( uiState = UiState.Success, - recordDetailInfo = RecordDetailModel( + recordDetailInfo = ReadingRecordModelV2( id = "", userBookId = "", pageNumber = 90, quote = "소설가들은 늘 소재를 찾아 떠도는 존재 같지만, 실은 그 반대인 경우가 더 잦다.", review = "소설가들은 늘 소재를 찾아 떠도는 존재 같지만, 실은 그 반대인 경우가 더 잦다", - emotionTags = listOf("따뜻함"), createdAt = "2023.10.10", updatedAt = "", bookTitle = "여름은 오래 그곳에 남아", @@ -214,13 +215,12 @@ private fun ReviewDetailEmptyPreview() { RecordDetailUi( state = RecordDetailUiState( uiState = UiState.Success, - recordDetailInfo = RecordDetailModel( + recordDetailInfo = ReadingRecordModelV2( id = "", userBookId = "", pageNumber = 90, quote = "소설가들은 늘 소재를 찾아 떠도는 존재 같지만, 실은 그 반대인 경우가 더 잦다.", review = "", - emotionTags = listOf("따뜻함"), createdAt = "2023.10.10", updatedAt = "", bookTitle = "여름은 오래 그곳에 남아", diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt index 07ff909a8..6a228b517 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt @@ -1,7 +1,7 @@ package com.ninecraft.booket.feature.detail.record import androidx.compose.runtime.Immutable -import com.ninecraft.booket.core.model.RecordDetailModel +import com.ninecraft.booket.core.model.ReadingRecordModelV2 import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import java.util.UUID @@ -16,7 +16,7 @@ sealed interface UiState { data class RecordDetailUiState( val uiState: UiState = UiState.Idle, - val recordDetailInfo: RecordDetailModel = RecordDetailModel(), + val recordDetailInfo: ReadingRecordModelV2 = ReadingRecordModelV2(), val isRecordMenuBottomSheetVisible: Boolean = false, val isRecordDeleteDialogVisible: Boolean = false, val sideEffect: RecordDetailSideEffect? = null, diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/QuoteItem.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/QuoteItem.kt index f32cc0c1e..02787b607 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/QuoteItem.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/QuoteItem.kt @@ -18,9 +18,11 @@ import com.ninecraft.booket.core.designsystem.theme.ReedTheme @Composable internal fun QuoteItem( quote: String, - page: Int, + page: Int?, modifier: Modifier = Modifier, ) { + val pageNumber = page?.toString() ?: "-" + Box( modifier = modifier .fillMaxWidth() @@ -38,7 +40,7 @@ internal fun QuoteItem( style = ReedTheme.typography.label1Medium, ) Text( - text = "${page}p", + text = "${pageNumber}p", modifier = Modifier.fillMaxWidth(), color = ReedTheme.colors.contentBrand, textAlign = TextAlign.End, diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewItem.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewItem.kt index bd4ebecde..b55217684 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewItem.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewItem.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -19,13 +20,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource +import com.ninecraft.booket.core.common.extensions.toFormattedDate import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.graphicRes import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.feature.detail.book.component.getEmotionImageResourceByDisplayName +import com.ninecraft.booket.core.model.DetailEmotionModel +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.PrimaryEmotionModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Composable internal fun ReviewItem( - emotion: String, + primaryEmotion: PrimaryEmotionModel, + detailEmotions: ImmutableList, createdAt: String, review: String, modifier: Modifier = Modifier, @@ -41,63 +49,170 @@ internal fun ReviewItem( ), ) { Column { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - painter = painterResource(getEmotionImageResourceByDisplayName(emotion)), - contentDescription = "Emotion Graphic", - modifier = Modifier - .size(ReedTheme.spacing.spacing10) - .clip(CircleShape) - .background(ReedTheme.colors.basePrimary), - ) - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) - Text( - text = "#$emotion", - color = ReedTheme.colors.contentBrand, - style = ReedTheme.typography.body2Medium, - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = createdAt, - color = ReedTheme.colors.contentTertiary, - style = ReedTheme.typography.label2Regular, - ) - } if (review.isNotBlank()) { - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) Text( text = review, color = ReedTheme.colors.contentSecondary, style = ReedTheme.typography.label1Medium, ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5)) } + EmotionContent(primaryEmotion, detailEmotions, createdAt) } } } +@Composable +private fun EmotionContent( + primaryEmotion: PrimaryEmotionModel, + detailEmotions: ImmutableList, + createdAt: String, +) { + val hasDetailEmotion = detailEmotions.isNotEmpty() + val primaryEmotionBackgroundColor = if (primaryEmotion.code == EmotionCode.OTHER) ReedTheme.colors.bgDisabled else ReedTheme.colors.bgTertiary + val primaryEmotionTextColor = if (primaryEmotion.code == EmotionCode.OTHER) ReedTheme.colors.contentTertiary else ReedTheme.colors.contentBrand + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(primaryEmotion.code.graphicRes), + contentDescription = "Emotion Graphic", + modifier = Modifier + .size(ReedTheme.spacing.spacing10) + .clip(CircleShape) + .background(ReedTheme.colors.basePrimary), + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = primaryEmotion.displayName, + modifier = Modifier + .background( + color = primaryEmotionBackgroundColor, + shape = RoundedCornerShape(ReedTheme.radius.full), + ) + .padding( + horizontal = ReedTheme.spacing.spacing2, + vertical = ReedTheme.spacing.spacing1, + ), + color = primaryEmotionTextColor, + style = ReedTheme.typography.label2SemiBold, + ) + + if (hasDetailEmotion) { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) + FlowRow { + detailEmotions.forEach { detail -> + Text( + text = "#${detail.name}", + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.caption1Regular, + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + } + } + } + } + Text( + text = createdAt.toFormattedDate(), + modifier = Modifier.align( + if (hasDetailEmotion) Alignment.Bottom else Alignment.CenterVertically, + ), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.label2Regular, + ) + } +} + @ComponentPreview @Composable -private fun ReviewBoxPreview() { +private fun ReviewItemPreview() { + val primaryEmotion = PrimaryEmotionModel( + code = EmotionCode.WARMTH, + displayName = "따뜻함", + ) + + val detailEmotions = persistentListOf( + DetailEmotionModel( + id = "84f95d93-e54c-11f0-8545-525ae7dd628c", + name = "위로받은", + ), + DetailEmotionModel( + id = "84f95e7e-e54c-11f0-8545-525ae7dd628c", + name = "포근한", + ), + DetailEmotionModel( + id = "84f95f13-e54c-11f0-8545-525ae7dd628c", + name = "다정한", + ), + DetailEmotionModel( + id = "84f95fc0-e54c-11f0-8545-525ae7dd628c", + name = "고마운", + ), + DetailEmotionModel( + id = "84f96094-e54c-11f0-8545-525ae7dd628c", + name = "마음이 놓이는", + ), + DetailEmotionModel( + id = "84f9612c-e54c-11f0-8545-525ae7dd628c", + name = "편안한", + ), + ) + ReedTheme { ReviewItem( - emotion = "따뜻함", + primaryEmotion = primaryEmotion, + detailEmotions = detailEmotions, review = "소설가들은 늘 소재를 찾아 떠도는 존재 같지만, 실은 그 반대인 경우가 더 잦다", - createdAt = "2025.06.25", + createdAt = "2026-01-08T15:31:36.113488", + ) + } +} + +@ComponentPreview +@Composable +private fun EmptyReviewItemPreview() { + val primaryEmotion = PrimaryEmotionModel( + code = EmotionCode.WARMTH, + displayName = "따뜻함", + ) + + val detailEmotions = persistentListOf( + DetailEmotionModel( + id = "84f95d93-e54c-11f0-8545-525ae7dd628c", + name = "위로받은", + ), + DetailEmotionModel( + id = "84f95e7e-e54c-11f0-8545-525ae7dd628c", + name = "포근한", + ), + ) + + ReedTheme { + ReviewItem( + primaryEmotion = primaryEmotion, + detailEmotions = detailEmotions, + review = "", + createdAt = "2026-01-08T15:31:36.113488", ) } } @ComponentPreview @Composable -private fun ReviewBoxEmptyPreview() { +private fun EmptyDetailEmotionsReviewItemPreview() { + val primaryEmotion = PrimaryEmotionModel( + code = EmotionCode.WARMTH, + displayName = "따뜻함", + ) ReedTheme { ReviewItem( - emotion = "따뜻함", + primaryEmotion = primaryEmotion, + detailEmotions = persistentListOf(), review = "", - createdAt = "2025.06.25", + createdAt = "2026-01-08T15:31:36.113488", ) } } diff --git a/feature/detail/src/main/res/drawable/img_record_card_other.webp b/feature/detail/src/main/res/drawable/img_record_card_other.webp new file mode 100644 index 000000000..b5d141255 Binary files /dev/null and b/feature/detail/src/main/res/drawable/img_record_card_other.webp differ diff --git a/feature/detail/stability/detail.stability b/feature/detail/stability/detail.stability index b7345b547..1722293b6 100644 --- a/feature/detail/stability/detail.stability +++ b/feature/detail/stability/detail.stability @@ -213,13 +213,13 @@ internal fun com.ninecraft.booket.feature.detail.card.RecordCardUi(state: com.ni - modifier: STABLE (marked @Stable or @Immutable) @Composable -internal fun com.ninecraft.booket.feature.detail.card.component.RecordCard(quote: kotlin.String, bookTitle: kotlin.String, emotion: kotlin.String, modifier: androidx.compose.ui.Modifier): kotlin.Unit +internal fun com.ninecraft.booket.feature.detail.card.component.RecordCard(quote: kotlin.String, bookTitle: kotlin.String, emotionCode: com.ninecraft.booket.core.model.EmotionCode, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: - quote: STABLE (String is immutable) - bookTitle: STABLE (String is immutable) - - emotion: STABLE (String is immutable) + - emotionCode: STABLE (class with no mutable properties) - modifier: STABLE (marked @Stable or @Immutable) @Composable @@ -263,12 +263,21 @@ internal fun com.ninecraft.booket.feature.detail.record.component.BookItem(image - modifier: STABLE (marked @Stable or @Immutable) @Composable -internal fun com.ninecraft.booket.feature.detail.record.component.QuoteItem(quote: kotlin.String, page: kotlin.Int, modifier: androidx.compose.ui.Modifier): kotlin.Unit +private fun com.ninecraft.booket.feature.detail.record.component.EmotionContent(primaryEmotion: com.ninecraft.booket.core.model.PrimaryEmotionModel, detailEmotions: kotlinx.collections.immutable.ImmutableList, createdAt: kotlin.String): kotlin.Unit + skippable: true + restartable: true + params: + - primaryEmotion: STABLE (marked @Stable or @Immutable) + - detailEmotions: STABLE (known stable type) + - createdAt: STABLE (String is immutable) + +@Composable +internal fun com.ninecraft.booket.feature.detail.record.component.QuoteItem(quote: kotlin.String, page: kotlin.Int?, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: - quote: STABLE (String is immutable) - - page: STABLE (primitive type) + - page: STABLE (class with no mutable properties) - modifier: STABLE (marked @Stable or @Immutable) @Composable @@ -296,11 +305,12 @@ private fun com.ninecraft.booket.feature.detail.record.component.RecordMenuItem( - modifier: STABLE (marked @Stable or @Immutable) @Composable -internal fun com.ninecraft.booket.feature.detail.record.component.ReviewItem(emotion: kotlin.String, createdAt: kotlin.String, review: kotlin.String, modifier: androidx.compose.ui.Modifier): kotlin.Unit +internal fun com.ninecraft.booket.feature.detail.record.component.ReviewItem(primaryEmotion: com.ninecraft.booket.core.model.PrimaryEmotionModel, detailEmotions: kotlinx.collections.immutable.ImmutableList, createdAt: kotlin.String, review: kotlin.String, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: - - emotion: STABLE (String is immutable) + - primaryEmotion: STABLE (marked @Stable or @Immutable) + - detailEmotions: STABLE (known stable type) - createdAt: STABLE (String is immutable) - review: STABLE (String is immutable) - modifier: STABLE (marked @Stable or @Immutable) diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditPresenter.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditPresenter.kt index a5245b56b..f24d41ee4 100644 --- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditPresenter.kt +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditPresenter.kt @@ -1,13 +1,23 @@ package com.ninecraft.booket.feature.edit.emotion import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.core.common.utils.handleException +import com.ninecraft.booket.core.data.api.repository.EmotionRepository +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.EmotionGroupModel import com.ninecraft.booket.feature.screens.EmotionEditScreen +import com.ninecraft.booket.feature.screens.EmotionEditScreen.Result +import com.ninecraft.booket.feature.screens.LoginScreen +import com.ninecraft.booket.feature.screens.arguments.DetailEmotionArg +import com.ninecraft.booket.feature.screens.arguments.PrimaryEmotionArg +import com.orhanobut.logger.Logger import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator @@ -16,12 +26,18 @@ import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch @AssistedInject class EmotionEditPresenter( @Assisted private val screen: EmotionEditScreen, @Assisted private val navigator: Navigator, + private val emotionRepository: EmotionRepository, ) : Presenter { @CircuitInject(EmotionEditScreen::class, AppScope::class) @@ -32,11 +48,55 @@ class EmotionEditPresenter( @Composable override fun present(): EmotionEditUiState { - var selectedEmotion by rememberRetained { mutableStateOf(screen.emotion) } - val emotions by rememberRetained { mutableStateOf(Emotion.entries.toPersistentList()) } + val scope = rememberCoroutineScope() + var emotionUiState by rememberRetained { mutableStateOf(EmotionUiState.Idle) } + var emotionGroups by rememberRetained { mutableStateOf(persistentListOf()) } + var selectedEmotionCode by rememberRetained { mutableStateOf(null) } + var selectedEmotionMap by rememberRetained { mutableStateOf>>(persistentMapOf()) } + var committedEmotionCode by rememberRetained { mutableStateOf(null) } + var committedEmotionMap by rememberRetained { mutableStateOf>>(persistentMapOf()) } + var isEmotionDetailBottomSheetVisible by rememberRetained { mutableStateOf(false) } val isEditButtonEnabled by remember { derivedStateOf { - selectedEmotion != screen.emotion + val originalEmotionCode = screen.primaryEmotionCode + val originalDetailIds = screen.detailEmotionIds.toSet() + + val currentEmotionCode = committedEmotionCode + val currentDetailIds = committedEmotionMap[currentEmotionCode].orEmpty().toSet() + + val isPrimaryEmotionChanged = originalEmotionCode != currentEmotionCode + val isDetailEmotionChanged = originalDetailIds != currentDetailIds + + isPrimaryEmotionChanged || isDetailEmotionChanged + } + } + + fun getEmotionGroups() { + scope.launch { + emotionUiState = EmotionUiState.Loading + emotionRepository.getEmotions() + .onSuccess { result -> + emotionUiState = EmotionUiState.Success + emotionGroups = result.emotions.toPersistentList() + selectedEmotionCode = screen.primaryEmotionCode + selectedEmotionMap = persistentMapOf(screen.primaryEmotionCode to screen.detailEmotionIds.toPersistentList()) + committedEmotionCode = screen.primaryEmotionCode + committedEmotionMap = persistentMapOf(screen.primaryEmotionCode to screen.detailEmotionIds.toPersistentList()) + }.onFailure { exception -> + emotionUiState = EmotionUiState.Error(exception) + + val handleErrorMessage = { message: String -> + Logger.e(message) + } + + handleException( + exception = exception, + onError = handleErrorMessage, + onLoginRequired = { + navigator.resetRoot(LoginScreen()) + }, + ) + } } } @@ -46,19 +106,116 @@ class EmotionEditPresenter( navigator.pop() } - is EmotionEditUiEvent.OnSelectEmotion -> { - selectedEmotion = event.emotion + is EmotionEditUiEvent.OnEditButtonClick -> { + val committedCode = committedEmotionCode ?: EmotionCode.OTHER + val committedDetailIds = committedEmotionMap[committedCode].orEmpty() + + val primaryEmotionArg = emotionGroups.firstOrNull { it.code == committedCode } + ?.let { + PrimaryEmotionArg( + code = it.code, + displayName = it.displayName, + ) + } + ?: PrimaryEmotionArg( + code = EmotionCode.OTHER, + displayName = "기타", + ) + + val detailEmotionArgs = + emotionGroups + .firstOrNull { it.code == committedCode } + ?.detailEmotions + ?.filter { it.id in committedDetailIds } + ?.map { + DetailEmotionArg( + id = it.id, + name = it.name, + ) + } + .orEmpty() + + navigator.pop( + result = Result( + primaryEmotion = primaryEmotionArg, + detailEmotions = detailEmotionArgs, + ), + ) } - is EmotionEditUiEvent.OnEditButtonClick -> { - navigator.pop(result = EmotionEditScreen.Result(selectedEmotion)) + is EmotionEditUiEvent.OnSelectEmotionCode -> { + selectedEmotionCode = event.emotionCode + + if (selectedEmotionCode == EmotionCode.OTHER) { + committedEmotionCode = selectedEmotionCode + committedEmotionMap = persistentMapOf() + selectedEmotionMap = persistentMapOf() + } else { + isEmotionDetailBottomSheetVisible = true + } + } + + is EmotionEditUiEvent.OnEmotionDetailToggled -> { + val emotionKey = selectedEmotionCode ?: return + val currentDetails = selectedEmotionMap[selectedEmotionCode].orEmpty() + val updatedDetails = if (event.detailId in currentDetails) { + currentDetails - event.detailId + } else { + currentDetails + event.detailId + } + + selectedEmotionMap = selectedEmotionMap.put(emotionKey, updatedDetails.toPersistentList()) + } + + is EmotionEditUiEvent.OnEmotionDetailRemoved -> { + val emotionKey = selectedEmotionCode ?: return + val currentDetails = committedEmotionMap[selectedEmotionCode].orEmpty() + val updatedDetails = currentDetails - event.detailId + + committedEmotionMap = committedEmotionMap.put(emotionKey, updatedDetails.toPersistentList()) + selectedEmotionMap = selectedEmotionMap.put(emotionKey, updatedDetails.toPersistentList()) + } + + is EmotionEditUiEvent.OnEmotionDetailCommitted -> { + val emotionKey = selectedEmotionCode ?: return + val details = selectedEmotionMap[emotionKey] ?: persistentListOf() + + committedEmotionCode = emotionKey + committedEmotionMap = persistentMapOf(emotionKey to details) + selectedEmotionMap = persistentMapOf(emotionKey to details) + isEmotionDetailBottomSheetVisible = false + } + + is EmotionEditUiEvent.OnEmotionDetailSkipped -> { + committedEmotionCode = selectedEmotionCode + // 건너뛰기 시 세부감정 선택 초기화 + committedEmotionMap = persistentMapOf() + selectedEmotionMap = persistentMapOf() + isEmotionDetailBottomSheetVisible = false + } + + is EmotionEditUiEvent.OnEmotionDetailBottomSheetDismiss -> { + isEmotionDetailBottomSheetVisible = false + } + + EmotionEditUiEvent.OnRetryGetEmotions -> { + getEmotionGroups() } } } + LaunchedEffect(Unit) { + getEmotionGroups() + } + return EmotionEditUiState( - selectedEmotion = selectedEmotion, - emotions = emotions, + emotionUiState = emotionUiState, + emotionGroups = emotionGroups, + selectedEmotionCode = selectedEmotionCode, + selectedEmotionMap = selectedEmotionMap, + committedEmotionCode = committedEmotionCode, + committedEmotionMap = committedEmotionMap, + isEmotionDetailBottomSheetVisible = isEmotionDetailBottomSheetVisible, isEditButtonEnabled = isEditButtonEnabled, eventSink = ::handleEvent, ) diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUi.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUi.kt index b9b93680d..7915db93a 100644 --- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUi.kt +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUi.kt @@ -1,9 +1,6 @@ package com.ninecraft.booket.feature.edit.emotion -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -11,36 +8,37 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.common.extensions.clickableSingle +import com.ninecraft.booket.core.common.extensions.toErrorType import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle -import com.ninecraft.booket.core.designsystem.graphicRes import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White -import com.ninecraft.booket.core.model.Emotion import com.ninecraft.booket.core.ui.ReedScaffold import com.ninecraft.booket.core.ui.component.ReedBackTopAppBar +import com.ninecraft.booket.core.ui.component.ReedErrorUi +import com.ninecraft.booket.core.ui.component.ReedLoadingIndicator import com.ninecraft.booket.feature.edit.R +import com.ninecraft.booket.feature.edit.emotion.component.EmotionDetailBottomSheet +import com.ninecraft.booket.feature.edit.emotion.component.EmotionItem import com.ninecraft.booket.feature.screens.EmotionEditScreen import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dev.zacsweers.metro.AppScope -import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.launch @TraceRecomposition @CircuitInject(EmotionEditScreen::class, AppScope::class) @@ -63,56 +61,88 @@ internal fun EmotionEditUi( state.eventSink(EmotionEditUiEvent.OnBackClick) }, ) - EmotionEditContent(state = state) + when (state.emotionUiState) { + is EmotionUiState.Idle -> {} + is EmotionUiState.Loading -> { + ReedLoadingIndicator() + } + + is EmotionUiState.Success -> { + EmotionEditContent(state = state) + } + + is EmotionUiState.Error -> { + ReedErrorUi( + errorType = state.emotionUiState.exception.toErrorType(), + onRetryClick = { state.eventSink(EmotionEditUiEvent.OnRetryGetEmotions) }, + ) + } + } } } } +@OptIn(ExperimentalMaterial3Api::class) @TraceRecomposition @Composable private fun EmotionEditContent( state: EmotionEditUiState, modifier: Modifier = Modifier, ) { - Column( + val emotionDetailBottomSheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + + Box( modifier = modifier .fillMaxSize() - .padding( - start = ReedTheme.spacing.spacing5, - top = ReedTheme.spacing.spacing4, - end = ReedTheme.spacing.spacing5, - ), + .background(color = White), ) { - Text( - text = stringResource(R.string.edit_emotion_title), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.heading1Bold, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) - Text( - text = stringResource(R.string.edit_emotion_description), - color = ReedTheme.colors.contentTertiary, - style = ReedTheme.typography.label1Medium, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) - LazyVerticalGrid( - columns = GridCells.Fixed(2), - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing3), - horizontalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing3), - content = { - items(state.emotions) { tag -> - EmotionItem( - emotion = tag, - onClick = { - state.eventSink(EmotionEditUiEvent.OnSelectEmotion(tag.displayName)) - }, - isSelected = state.selectedEmotion == tag.displayName, - modifier = Modifier.fillMaxWidth(), - ) - } - }, - ) + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = ReedTheme.spacing.spacing5) + .padding(bottom = 80.dp), + ) { + item { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) + } + item { + Text( + text = stringResource(R.string.edit_emotion_title), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.heading1Bold, + ) + } + item { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) + } + item { + Text( + text = stringResource(R.string.edit_emotion_description), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.label1Medium, + ) + } + item { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) + } + + items(state.emotionGroups) { emotion -> + EmotionItem( + emotionGroup = emotion, + selectedEmotionDetailIds = state.committedEmotionMap[emotion.code] ?: persistentListOf(), + onClick = { + state.eventSink(EmotionEditUiEvent.OnSelectEmotionCode(emotion.code)) + }, + isSelected = state.committedEmotionCode == emotion.code, + onEmotionDetailRemove = { detail -> + state.eventSink(EmotionEditUiEvent.OnEmotionDetailRemoved(detail)) + }, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + } + } + ReedButton( onClick = { state.eventSink(EmotionEditUiEvent.OnEditButtonClick) @@ -121,43 +151,44 @@ private fun EmotionEditContent( sizeStyle = largeButtonStyle, modifier = Modifier .fillMaxWidth() - .padding(vertical = ReedTheme.spacing.spacing4), + .align(Alignment.BottomCenter) + .padding(horizontal = ReedTheme.spacing.spacing5) + .padding(bottom = ReedTheme.spacing.spacing4), enabled = state.isEditButtonEnabled, text = stringResource(R.string.edit_emotion_edit), ) } -} -@Composable -private fun EmotionItem( - emotion: Emotion, - onClick: () -> Unit, - isSelected: Boolean, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .height(214.dp) - .clip(RoundedCornerShape(ReedTheme.radius.md)) - .background(color = ReedTheme.colors.bgTertiary) - .then( - if (isSelected) Modifier.border( - width = 2.dp, - color = ReedTheme.colors.borderBrand, - shape = RoundedCornerShape(ReedTheme.radius.md), - ) - else Modifier, - ) - .clickableSingle { - onClick() + if (state.isEmotionDetailBottomSheetVisible) { + val selectedEmotionGroup = state.emotionGroups.firstOrNull { it.code == state.selectedEmotionCode } ?: return + EmotionDetailBottomSheet( + emotionGroup = selectedEmotionGroup, + selectedEmotionDetailIds = state.selectedEmotionMap[state.selectedEmotionCode] ?: persistentListOf(), + onDismissRequest = { + state.eventSink(EmotionEditUiEvent.OnEmotionDetailBottomSheetDismiss) + }, + sheetState = emotionDetailBottomSheetState, + onCloseButtonClick = { + coroutineScope.launch { + emotionDetailBottomSheetState.hide() + state.eventSink(EmotionEditUiEvent.OnEmotionDetailBottomSheetDismiss) + } + }, + onEmotionDetailToggled = { detail -> + state.eventSink(EmotionEditUiEvent.OnEmotionDetailToggled(detail)) + }, + onSkipButtonClick = { + coroutineScope.launch { + emotionDetailBottomSheetState.hide() + state.eventSink(EmotionEditUiEvent.OnEmotionDetailSkipped) + } + }, + onConfirmButtonClick = { + coroutineScope.launch { + emotionDetailBottomSheetState.hide() + state.eventSink(EmotionEditUiEvent.OnEmotionDetailCommitted) + } }, - contentAlignment = Alignment.Center, - ) { - Image( - painter = painterResource(emotion.graphicRes), - contentDescription = "Emotion Image", - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, ) } } @@ -166,11 +197,8 @@ private fun EmotionItem( @Composable private fun EmotionEditUiPreview() { ReedTheme { - val emotions = Emotion.entries.toPersistentList() - EmotionEditUi( state = EmotionEditUiState( - emotions = emotions, eventSink = {}, ), ) diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUiState.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUiState.kt index 4a4bb3998..18390fe7c 100644 --- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUiState.kt +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/EmotionEditUiState.kt @@ -1,20 +1,43 @@ package com.ninecraft.booket.feature.edit.emotion -import com.ninecraft.booket.core.model.Emotion +import androidx.compose.runtime.Immutable +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.EmotionGroupModel import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf + +@Immutable +sealed interface EmotionUiState { + data object Idle : EmotionUiState + data object Loading : EmotionUiState + data object Success : EmotionUiState + data class Error(val exception: Throwable) : EmotionUiState +} data class EmotionEditUiState( - val selectedEmotion: String = "", + val emotionUiState: EmotionUiState = EmotionUiState.Idle, val isEditButtonEnabled: Boolean = false, - val emotions: ImmutableList = persistentListOf(), + val emotionGroups: ImmutableList = persistentListOf(), + val selectedEmotionCode: EmotionCode? = null, + val selectedEmotionMap: PersistentMap> = persistentMapOf(), + val committedEmotionCode: EmotionCode? = null, + val committedEmotionMap: PersistentMap> = persistentMapOf(), + val isEmotionDetailBottomSheetVisible: Boolean = false, val eventSink: (EmotionEditUiEvent) -> Unit, ) : CircuitUiState sealed interface EmotionEditUiEvent : CircuitUiEvent { data object OnBackClick : EmotionEditUiEvent - data class OnSelectEmotion(val emotion: String) : EmotionEditUiEvent + data class OnSelectEmotionCode(val emotionCode: EmotionCode) : EmotionEditUiEvent + data class OnEmotionDetailToggled(val detailId: String) : EmotionEditUiEvent + data class OnEmotionDetailRemoved(val detailId: String) : EmotionEditUiEvent + data object OnEmotionDetailCommitted : EmotionEditUiEvent + data object OnEmotionDetailSkipped : EmotionEditUiEvent + data object OnEmotionDetailBottomSheetDismiss : EmotionEditUiEvent data object OnEditButtonClick : EmotionEditUiEvent + data object OnRetryGetEmotions : EmotionEditUiEvent } diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/component/EmotionDetailBottomSheet.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/component/EmotionDetailBottomSheet.kt new file mode 100644 index 000000000..5a208ddf5 --- /dev/null +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/component/EmotionDetailBottomSheet.kt @@ -0,0 +1,202 @@ +package com.ninecraft.booket.feature.edit.emotion.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import com.ninecraft.booket.core.common.extensions.clickableSingle +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.component.button.ReedButton +import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle +import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle +import com.ninecraft.booket.core.designsystem.component.chip.ReedSelectableChip +import com.ninecraft.booket.core.designsystem.component.chip.mediumChipStyle +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.core.model.DetailEmotionModel +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.EmotionGroupModel +import com.ninecraft.booket.core.ui.component.ReedBottomSheet +import com.ninecraft.booket.feature.edit.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import com.ninecraft.booket.core.designsystem.R as designR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun EmotionDetailBottomSheet( + emotionGroup: EmotionGroupModel, + selectedEmotionDetailIds: ImmutableList, + onDismissRequest: () -> Unit, + sheetState: SheetState, + onCloseButtonClick: () -> Unit, + onEmotionDetailToggled: (String) -> Unit, + onSkipButtonClick: () -> Unit, + onConfirmButtonClick: () -> Unit, +) { + ReedBottomSheet( + onDismissRequest = { + onDismissRequest() + }, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .padding( + start = ReedTheme.spacing.spacing5, + top = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.edit_emotion_detail_title, emotionGroup.displayName), + color = ReedTheme.colors.contentPrimary, + textAlign = TextAlign.Center, + style = ReedTheme.typography.heading2SemiBold, + ) + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_close), + contentDescription = "Close Icon", + modifier = Modifier.clickableSingle { + onCloseButtonClick() + }, + ) + } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) + Text( + text = stringResource(R.string.edit_emotion_detail_description), + modifier = Modifier.fillMaxWidth(), + color = ReedTheme.colors.contentSecondary, + style = ReedTheme.typography.label1Medium, + ) + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding( + start = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + top = ReedTheme.spacing.spacing6, + bottom = ReedTheme.spacing.spacing3, + ), + horizontalArrangement = Arrangement.spacedBy( + ReedTheme.spacing.spacing2, + Alignment.CenterHorizontally, + ), + verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), + ) { + emotionGroup.detailEmotions.forEach { detail -> + ReedSelectableChip( + label = detail.name, + chipSizeStyle = mediumChipStyle, + selected = detail.id in selectedEmotionDetailIds, + onClick = { + onEmotionDetailToggled(detail.id) + }, + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = ReedTheme.spacing.spacing4), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + ReedButton( + onClick = { + onSkipButtonClick() + }, + text = stringResource(R.string.edit_emotion_detail_skip), + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.SECONDARY, + modifier = Modifier.weight(1f), + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + ReedButton( + onClick = { + onConfirmButtonClick() + }, + text = stringResource(R.string.edit_emotion_detail_confirm), + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.PRIMARY, + modifier = Modifier.weight(1f), + enabled = selectedEmotionDetailIds.isNotEmpty(), + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@ComponentPreview +@Composable +private fun EmotionDetailBottomSheetPreview() { + val warmthEmotionGroup = EmotionGroupModel( + code = EmotionCode.WARMTH, + displayName = "따뜻함", + detailEmotions = persistentListOf( + DetailEmotionModel( + id = "84f95d93-e54c-11f0-8545-525ae7dd628c", + name = "위로받은", + ), + DetailEmotionModel( + id = "84f95e7e-e54c-11f0-8545-525ae7dd628c", + name = "포근한", + ), + DetailEmotionModel( + id = "84f95f13-e54c-11f0-8545-525ae7dd628c", + name = "다정한", + ), + DetailEmotionModel( + id = "84f95fc0-e54c-11f0-8545-525ae7dd628c", + name = "고마운", + ), + DetailEmotionModel( + id = "84f96094-e54c-11f0-8545-525ae7dd628c", + name = "마음이 놓이는", + ), + DetailEmotionModel( + id = "84f9612c-e54c-11f0-8545-525ae7dd628c", + name = "편안한", + ), + ), + ) + val sheetState = SheetState( + skipPartiallyExpanded = true, + initialValue = SheetValue.Expanded, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + ) + ReedTheme { + EmotionDetailBottomSheet( + emotionGroup = warmthEmotionGroup, + selectedEmotionDetailIds = persistentListOf(), + onDismissRequest = {}, + sheetState = sheetState, + onCloseButtonClick = {}, + onSkipButtonClick = {}, + onConfirmButtonClick = {}, + onEmotionDetailToggled = {}, + ) + } +} diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/component/EmotionItem.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/component/EmotionItem.kt new file mode 100644 index 000000000..4385e5f59 --- /dev/null +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/emotion/component/EmotionItem.kt @@ -0,0 +1,181 @@ +package com.ninecraft.booket.feature.edit.emotion.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.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.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.R +import com.ninecraft.booket.core.designsystem.component.chip.ReedRemovableChip +import com.ninecraft.booket.core.designsystem.component.chip.smallChipStyle +import com.ninecraft.booket.core.designsystem.descriptionRes +import com.ninecraft.booket.core.designsystem.categoryGraphicRes +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.core.model.DetailEmotionModel +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.EmotionGroupModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun EmotionItem( + emotionGroup: EmotionGroupModel, + selectedEmotionDetailIds: ImmutableList, + onClick: () -> Unit, + isSelected: Boolean, + onEmotionDetailRemove: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val cornerShape = RoundedCornerShape(ReedTheme.radius.md) + val iconRes = if (isSelected) R.drawable.ic_check else R.drawable.ic_chevron_right + val iconTint = if (isSelected) ReedTheme.colors.borderBrand else ReedTheme.colors.contentTertiary + + Column( + modifier = modifier + .fillMaxWidth() + .clip(cornerShape) + .clickable { + onClick() + } + .background(color = ReedTheme.colors.baseSecondary) + .then( + if (isSelected) Modifier.border( + width = ReedTheme.border.border15, + color = ReedTheme.colors.borderBrand, + shape = cornerShape, + ) + else Modifier, + ) + .padding( + horizontal = ReedTheme.spacing.spacing4, + vertical = ReedTheme.spacing.spacing3, + ), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + val emotionGraphicRes = emotionGroup.code.categoryGraphicRes + if (emotionGraphicRes != null) { + Image( + painter = painterResource(emotionGraphicRes), + contentDescription = "Emotion Image", + modifier = Modifier + .size(60.dp) + .clip(CircleShape), + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4)) + } + + Column { + Text( + text = emotionGroup.displayName, + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.headline1SemiBold, + ) + Text( + text = stringResource(emotionGroup.code.descriptionRes), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.label1Medium, + ) + } + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = ImageVector.vectorResource(iconRes), + contentDescription = "Chevron Right", + tint = iconTint, + ) + } + + if (selectedEmotionDetailIds.isNotEmpty()) { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), + verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), + ) { + selectedEmotionDetailIds.forEach { detailId -> + val detailName = emotionGroup.detailEmotions.firstOrNull { it.id == detailId }?.name ?: return@forEach + ReedRemovableChip( + label = detailName, + chipSizeStyle = smallChipStyle, + onRemove = { + onEmotionDetailRemove(detailId) + }, + ) + } + } + } + } +} + +@ComponentPreview +@Composable +private fun EmotionItemPreview() { + val warmthEmotionGroup = EmotionGroupModel( + code = EmotionCode.WARMTH, + displayName = "따뜻함", + detailEmotions = persistentListOf( + DetailEmotionModel( + id = "84f95d93-e54c-11f0-8545-525ae7dd628c", + name = "위로받은", + ), + DetailEmotionModel( + id = "84f95e7e-e54c-11f0-8545-525ae7dd628c", + name = "포근한", + ), + DetailEmotionModel( + id = "84f95f13-e54c-11f0-8545-525ae7dd628c", + name = "다정한", + ), + DetailEmotionModel( + id = "84f95fc0-e54c-11f0-8545-525ae7dd628c", + name = "고마운", + ), + DetailEmotionModel( + id = "84f96094-e54c-11f0-8545-525ae7dd628c", + name = "마음이 놓이는", + ), + DetailEmotionModel( + id = "84f9612c-e54c-11f0-8545-525ae7dd628c", + name = "편안한", + ), + ), + ) + + val selectedEmotionDetailIds = persistentListOf( + "84f95fc0-e54c-11f0-8545-525ae7dd628c", + "84f96094-e54c-11f0-8545-525ae7dd628c", + ) + + ReedTheme { + EmotionItem( + emotionGroup = warmthEmotionGroup, + selectedEmotionDetailIds = selectedEmotionDetailIds, + onClick = {}, + isSelected = false, + onEmotionDetailRemove = {}, + modifier = Modifier.fillMaxWidth(), + ) + } +} diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt index da4a22ad1..c1752ffb2 100644 --- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditPresenter.kt @@ -52,7 +52,7 @@ class RecordEditPresenter( override fun present(): RecordEditUiState { val scope = rememberCoroutineScope() var recordInfo by rememberRetained { mutableStateOf(screen.recordInfo) } - val recordPageState = rememberTextFieldState(recordInfo.pageNumber.toString()) + val recordPageState = rememberTextFieldState(recordInfo.pageNumber?.toString() ?: "") val recordQuoteState = rememberTextFieldState(recordInfo.quote) val recordImpressionState = rememberTextFieldState(recordInfo.review) val isPageError by remember { @@ -63,32 +63,38 @@ class RecordEditPresenter( } val hasChanges by remember { derivedStateOf { - val pageChanged = recordPageState.text.toString() != recordInfo.pageNumber.toString() + val pageChanged = recordPageState.text.toString().toIntOrNull() != recordInfo.pageNumber val quoteChanged = recordQuoteState.text.toString() != recordInfo.quote val impressionChanged = recordImpressionState.text.toString() != recordInfo.review - val emotionChanged = recordInfo.emotionTags != screen.recordInfo.emotionTags + + val originalPrimaryEmotionCode = screen.recordInfo.primaryEmotion.code + val originalDetailEmotionIds = screen.recordInfo.detailEmotions.map { it.id }.toSet() + val currentPrimaryEmotionCode = recordInfo.primaryEmotion.code + val currentDetailEmotionIds = recordInfo.detailEmotions.map { it.id }.toSet() + val emotionChanged = originalPrimaryEmotionCode != currentPrimaryEmotionCode || originalDetailEmotionIds != currentDetailEmotionIds pageChanged || quoteChanged || impressionChanged || emotionChanged } } val isSaveButtonEnabled by remember { derivedStateOf { - recordPageState.text.isNotEmpty() && - recordQuoteState.text.isNotEmpty() && - !isPageError && - hasChanges + recordQuoteState.text.isNotEmpty() && !isPageError && hasChanges } } var sideEffect by rememberRetained { mutableStateOf(null) } val emotionEditNavigator = rememberAnsweringNavigator(navigator) { result -> - recordInfo = recordInfo.copy(emotionTags = listOf(result.emotion)) + recordInfo = recordInfo.copy( + primaryEmotion = result.primaryEmotion, + detailEmotions = result.detailEmotions, + ) } fun editRecord( readingRecordId: String, - pageNumber: Int, + pageNumber: Int?, quote: String, - emotionTags: List, + primaryEmotion: String, + detailEmotionIds: List, impression: String, onSuccess: () -> Unit = {}, ) { @@ -97,8 +103,9 @@ class RecordEditPresenter( readingRecordId = readingRecordId, pageNumber = pageNumber, quote = quote, - emotionTags = emotionTags, review = impression, + primaryEmotion = primaryEmotion, + detailEmotionTagIds = detailEmotionIds, ).onSuccess { analyticsHelper.logEvent(RECORD_EDIT_SAVE) onSuccess() @@ -130,16 +137,21 @@ class RecordEditPresenter( } RecordEditUiEvent.OnEmotionEditClick -> { - val emotion = recordInfo.emotionTags.firstOrNull() ?: "" - emotionEditNavigator.goTo(EmotionEditScreen(emotion)) + emotionEditNavigator.goTo( + EmotionEditScreen( + primaryEmotionCode = recordInfo.primaryEmotion.code, + detailEmotionIds = recordInfo.detailEmotions.map { it.id }, + ), + ) } RecordEditUiEvent.OnSaveButtonClick -> { editRecord( readingRecordId = recordInfo.id, - pageNumber = recordPageState.text.toString().toIntOrNull() ?: 0, + pageNumber = recordPageState.text.toString().toIntOrNull(), quote = recordQuoteState.text.toString(), - emotionTags = recordInfo.emotionTags, + primaryEmotion = recordInfo.primaryEmotion.code.name, + detailEmotionIds = recordInfo.detailEmotions.map { it.id }, impression = recordImpressionState.text.toString(), onSuccess = { navigator.pop() diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt index 5225c3431..5373d776e 100644 --- a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/RecordEditUi.kt @@ -1,9 +1,7 @@ package com.ninecraft.booket.feature.edit.record -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.exclude @@ -13,21 +11,16 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp @@ -39,15 +32,20 @@ import com.ninecraft.booket.core.designsystem.component.textfield.ReedRecordText import com.ninecraft.booket.core.designsystem.component.textfield.digitOnlyInputTransformation import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White +import com.ninecraft.booket.core.model.EmotionCode import com.ninecraft.booket.core.ui.ReedScaffold import com.ninecraft.booket.core.ui.component.ReedTopAppBar import com.ninecraft.booket.feature.edit.R import com.ninecraft.booket.feature.edit.record.component.BookItem +import com.ninecraft.booket.feature.edit.record.component.EmotionItem import com.ninecraft.booket.feature.screens.RecordEditScreen +import com.ninecraft.booket.feature.screens.arguments.DetailEmotionArg +import com.ninecraft.booket.feature.screens.arguments.PrimaryEmotionArg import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject import dev.zacsweers.metro.AppScope +import kotlinx.collections.immutable.toPersistentList import com.ninecraft.booket.core.designsystem.R as designR @TraceRecomposition @@ -152,7 +150,7 @@ private fun ColumnScope.RecordEditContent(state: RecordEditUiState) { ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) Text( - text = stringResource(R.string.edit_record_impression_label), + text = stringResource(R.string.edit_record_memo_label), color = ReedTheme.colors.contentPrimary, style = ReedTheme.typography.body1Medium, ) @@ -169,36 +167,20 @@ private fun ColumnScope.RecordEditContent(state: RecordEditUiState) { ), ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.edit_record_emotion_label), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.body1Medium, - ) - Spacer(modifier = Modifier.weight(1f)) - Row( - modifier = Modifier.clickable { - state.eventSink(RecordEditUiEvent.OnEmotionEditClick) - }, - ) { - val emotion = state.recordInfo.emotionTags.firstOrNull() ?: "" - - Text( - text = emotion, - color = ReedTheme.colors.contentSecondary, - style = ReedTheme.typography.body1Medium, - ) - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing1)) - Icon( - imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_right), - contentDescription = "Chevron Right Icon", - tint = ReedTheme.colors.contentSecondary, - ) - } - } + Text( + text = stringResource(R.string.edit_record_emotion_label), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) + EmotionItem( + primaryEmotionCode = state.recordInfo.primaryEmotion.code, + primaryEmotionName = state.recordInfo.primaryEmotion.displayName, + detailEmotions = state.recordInfo.detailEmotions.toPersistentList(), + onClick = { + state.eventSink(RecordEditUiEvent.OnEmotionEditClick) + }, + ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing16)) } } @@ -230,7 +212,20 @@ private fun RecordEditUiPreview() { pageNumber = 33, quote = "소설가들은 늘 소재를 찾아 떠도는 존재 같지만, 실은 그 반대인 경우가 더 잦다.", review = "감동적이었다.", - emotionTags = listOf("따뜻함"), + primaryEmotion = PrimaryEmotionArg( + code = EmotionCode.WARMTH, + displayName = "따뜻함", + ), + detailEmotions = listOf( + DetailEmotionArg( + id = "84f95d93-e54c-11f0-8545-525ae7dd628c", + name = "위로받은", + ), + DetailEmotionArg( + id = "84f95e7e-e54c-11f0-8545-525ae7dd628c", + name = "포근한", + ), + ), bookTitle = "여름은 오래 그곳에 남아", bookPublisher = "비채", bookCoverImageUrl = "", diff --git a/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/component/EmotionItem.kt b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/component/EmotionItem.kt new file mode 100644 index 000000000..1084cca6f --- /dev/null +++ b/feature/edit/src/main/kotlin/com/ninecraft/booket/feature/edit/record/component/EmotionItem.kt @@ -0,0 +1,147 @@ +package com.ninecraft.booket.feature.edit.record.component + +import androidx.compose.foundation.Image +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.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.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.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.R +import com.ninecraft.booket.core.designsystem.graphicRes +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.feature.screens.arguments.DetailEmotionArg +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun EmotionItem( + primaryEmotionCode: EmotionCode, + primaryEmotionName: String, + detailEmotions: ImmutableList, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(ReedTheme.radius.md)) + .background(color = ReedTheme.colors.baseSecondary) + .clickable { + onClick() + } + .padding( + horizontal = ReedTheme.spacing.spacing4, + vertical = ReedTheme.spacing.spacing4, + ), + ) { + EmotionContent(primaryEmotionCode, primaryEmotionName, detailEmotions) + } +} + +@Composable +private fun EmotionContent( + primaryEmotionCode: EmotionCode, + primaryEmotionName: String, + detailEmotions: ImmutableList, +) { + val hasDetailEmotion = detailEmotions.isNotEmpty() + val primaryEmotionBackgroundColor = if (primaryEmotionCode == EmotionCode.OTHER) ReedTheme.colors.bgDisabled else ReedTheme.colors.bgTertiary + val primaryEmotionTextColor = if (primaryEmotionCode == EmotionCode.OTHER) ReedTheme.colors.contentTertiary else ReedTheme.colors.contentBrand + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(primaryEmotionCode.graphicRes), + contentDescription = "Emotion Graphic", + modifier = Modifier + .size(ReedTheme.spacing.spacing10) + .clip(CircleShape) + .background(ReedTheme.colors.basePrimary), + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + Column { + Text( + text = primaryEmotionName, + modifier = Modifier + .background( + color = primaryEmotionBackgroundColor, + shape = RoundedCornerShape(ReedTheme.radius.full), + ) + .padding( + horizontal = ReedTheme.spacing.spacing2, + vertical = ReedTheme.spacing.spacing1, + ), + color = primaryEmotionTextColor, + style = ReedTheme.typography.label2SemiBold, + ) + + if (hasDetailEmotion) { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) + FlowRow { + detailEmotions.forEach { detail -> + Text( + text = "#${detail.name}", + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.caption1Regular, + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + } + } + } + } + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right), + contentDescription = "Chevron Right Icon", + tint = ReedTheme.colors.contentSecondary, + ) + } +} + +@ComponentPreview +@Composable +private fun EmotionItemPreview() { + val primaryEmotionName = "따뜻함" + + val detailEmotions = persistentListOf( + DetailEmotionArg( + id = "84f95d93-e54c-11f0-8545-525ae7dd628c", + name = "위로받은", + ), + DetailEmotionArg( + id = "84f95e7e-e54c-11f0-8545-525ae7dd628c", + name = "포근한", + ), + ) + + ReedTheme { + EmotionItem( + primaryEmotionName = primaryEmotionName, + primaryEmotionCode = EmotionCode.WARMTH, + detailEmotions = detailEmotions, + onClick = {}, + ) + } +} diff --git a/feature/edit/src/main/res/values/strings.xml b/feature/edit/src/main/res/values/strings.xml index bd99c9a71..d3ca53560 100644 --- a/feature/edit/src/main/res/values/strings.xml +++ b/feature/edit/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ 독서 기록 수정 책 페이지 문장 기록 - 감상평 + 메모 감정 기록하고 싶은 페이지를 작성해보세요 해당 책의 마지막 페이지 수를 초과했습니다 @@ -13,4 +13,8 @@ 문장에 대해 어떤 감정이 드셨나요? 대표 감정을 한 가지 선택해주세요 수정하기 + 어떤 %1$s을 느꼈나요? + 더 자세한 감정을 선택 기록할 수 있어요. + 건너뛰기 + 선택 완료 diff --git a/feature/edit/stability/edit.stability b/feature/edit/stability/edit.stability index 5543c7f9e..72126dd27 100644 --- a/feature/edit/stability/edit.stability +++ b/feature/edit/stability/edit.stability @@ -27,13 +27,29 @@ internal fun com.ninecraft.booket.feature.edit.emotion.EmotionEditUi(state: com. - modifier: STABLE (marked @Stable or @Immutable) @Composable -private fun com.ninecraft.booket.feature.edit.emotion.EmotionItem(emotion: com.ninecraft.booket.core.model.Emotion, onClick: kotlin.Function0, isSelected: kotlin.Boolean, modifier: androidx.compose.ui.Modifier): kotlin.Unit +internal fun com.ninecraft.booket.feature.edit.emotion.component.EmotionDetailBottomSheet(emotionGroup: com.ninecraft.booket.core.model.EmotionGroupModel, selectedEmotionDetailIds: kotlinx.collections.immutable.ImmutableList, onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, onCloseButtonClick: kotlin.Function0, onEmotionDetailToggled: kotlin.Function1, onSkipButtonClick: kotlin.Function0, onConfirmButtonClick: kotlin.Function0): kotlin.Unit skippable: true restartable: true params: - - emotion: STABLE (class with no mutable properties) + - emotionGroup: STABLE (marked @Stable or @Immutable) + - selectedEmotionDetailIds: STABLE (known stable type) + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - onCloseButtonClick: STABLE (function type) + - onEmotionDetailToggled: STABLE (function type) + - onSkipButtonClick: STABLE (function type) + - onConfirmButtonClick: STABLE (function type) + +@Composable +internal fun com.ninecraft.booket.feature.edit.emotion.component.EmotionItem(emotionGroup: com.ninecraft.booket.core.model.EmotionGroupModel, selectedEmotionDetailIds: kotlinx.collections.immutable.ImmutableList, onClick: kotlin.Function0, isSelected: kotlin.Boolean, onEmotionDetailRemove: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - emotionGroup: STABLE (marked @Stable or @Immutable) + - selectedEmotionDetailIds: STABLE (known stable type) - onClick: STABLE (function type) - isSelected: STABLE (primitive type) + - onEmotionDetailRemove: STABLE (function type) - modifier: STABLE (marked @Stable or @Immutable) @Composable @@ -75,3 +91,23 @@ internal fun com.ninecraft.booket.feature.edit.record.component.BookItem(imageUr - publisher: STABLE (String is immutable) - modifier: STABLE (marked @Stable or @Immutable) +@Composable +private fun com.ninecraft.booket.feature.edit.record.component.EmotionContent(primaryEmotionCode: com.ninecraft.booket.core.model.EmotionCode, primaryEmotionName: kotlin.String, detailEmotions: kotlinx.collections.immutable.ImmutableList): kotlin.Unit + skippable: true + restartable: true + params: + - primaryEmotionCode: STABLE (class with no mutable properties) + - primaryEmotionName: STABLE (String is immutable) + - detailEmotions: STABLE (known stable type) + +@Composable +internal fun com.ninecraft.booket.feature.edit.record.component.EmotionItem(primaryEmotionCode: com.ninecraft.booket.core.model.EmotionCode, primaryEmotionName: kotlin.String, detailEmotions: kotlinx.collections.immutable.ImmutableList, onClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - primaryEmotionCode: STABLE (class with no mutable properties) + - primaryEmotionName: STABLE (String is immutable) + - detailEmotions: STABLE (known stable type) + - onClick: STABLE (function type) + - modifier: STABLE (marked @Stable or @Immutable) + diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/CustomTooltipBox.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/CustomTooltipBox.kt deleted file mode 100644 index 5fd7d65c1..000000000 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/CustomTooltipBox.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.ninecraft.booket.feature.record.component - -import androidx.annotation.StringRes -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.designsystem.ComponentPreview -import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.feature.record.R - -@Composable -internal fun CustomTooltipBox( - @StringRes messageResId: Int, -) { - Row(verticalAlignment = Alignment.CenterVertically) { - Box( - Modifier - .shadow(ReedTheme.radius.xs, RoundedCornerShape(ReedTheme.radius.xs), clip = false) - .clip(RoundedCornerShape(ReedTheme.radius.xs)) - .background(ReedTheme.colors.contentPrimary) - .padding( - horizontal = ReedTheme.spacing.spacing3, - vertical = ReedTheme.spacing.spacing2, - ), - ) { - Text( - text = stringResource(messageResId), - color = ReedTheme.colors.contentInverse, - style = ReedTheme.typography.label2Regular, - ) - } - Box( - Modifier - .padding(start = 2.dp) - .size(ReedTheme.spacing.spacing3) - .offset { - IntOffset( - x = (-10).dp.roundToPx(), - y = 0, - ) - } - .graphicsLayer { - rotationZ = 45f - shadowElevation = 8.dp.toPx() - clip = true - } - .background(ReedTheme.colors.contentPrimary), - ) - } -} - -@ComponentPreview -@Composable -private fun CustomTooltipBoxPreview() { - ReedTheme { - CustomTooltipBox(messageResId = R.string.scan_tooltip_message) - } -} diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt deleted file mode 100644 index 599b6dbd1..000000000 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.ninecraft.booket.feature.record.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextAlign -import com.ninecraft.booket.core.common.extensions.clickableSingle -import com.ninecraft.booket.core.designsystem.ComponentPreview -import com.ninecraft.booket.core.designsystem.component.button.ReedButton -import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle -import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle -import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.core.ui.component.ReedBottomSheet -import com.ninecraft.booket.feature.record.R -import com.skydoves.compose.stability.runtime.TraceRecomposition -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toPersistentList -import com.ninecraft.booket.core.designsystem.R as designR - -@TraceRecomposition -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ImpressionGuideBottomSheet( - onDismissRequest: () -> Unit, - sheetState: SheetState, - impressionState: TextFieldState, - impressionGuideList: ImmutableList, - beforeSelectedImpressionGuide: String, - selectedImpressionGuide: String, - onGuideClick: (Int) -> Unit, - onCloseButtonClick: () -> Unit, - onSelectionConfirmButtonClick: () -> Unit, -) { - ReedBottomSheet( - onDismissRequest = { - onDismissRequest() - }, - sheetState = sheetState, - ) { - Column( - modifier = Modifier - .padding( - start = ReedTheme.spacing.spacing5, - top = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - ), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResource(R.string.impression_step_guide), - color = ReedTheme.colors.contentPrimary, - textAlign = TextAlign.Center, - style = ReedTheme.typography.heading2SemiBold, - ) - Icon( - imageVector = ImageVector.vectorResource(designR.drawable.ic_close), - contentDescription = "Close Icon", - modifier = Modifier.clickableSingle { - onCloseButtonClick() - }, - ) - } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) - Text( - text = stringResource(R.string.impression_guide_description), - modifier = Modifier.fillMaxWidth(), - color = ReedTheme.colors.contentSecondary, - style = ReedTheme.typography.label1Medium, - ) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = ReedTheme.spacing.spacing5), - verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), - ) { - impressionGuideList.forEachIndexed { index, guide -> - ImpressionGuideBox( - onClick = { - onGuideClick(index) - }, - impressionText = guide, - isSelected = selectedImpressionGuide == guide, - ) - } - } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) - val isButtonEnabled = if (impressionState.text.isEmpty()) { - selectedImpressionGuide.isNotEmpty() - } else { - beforeSelectedImpressionGuide != selectedImpressionGuide - } - ReedButton( - onClick = { - onSelectionConfirmButtonClick() - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.PRIMARY, - modifier = Modifier.fillMaxWidth(), - enabled = isButtonEnabled, - text = stringResource(R.string.impression_guide_selection_done), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@ComponentPreview -@Composable -private fun ImpressionGuideBottomSheetPreview() { - val sheetState = SheetState( - skipPartiallyExpanded = true, - initialValue = SheetValue.Expanded, - positionalThreshold = { 0f }, - velocityThreshold = { 0f }, - ) - val impressionGuideList = listOf( - "에서 위로 받았다", - "이 마음에 남았다", - "에서 작가의 의도가 궁금하다", - "에 대한 다른 사람들의 생각이 궁금하다", - "에서 크게 공감이 된다", - "을 보고 예전 기억이 났다", - "에서 문장에 머물렀다", - ).toPersistentList() - - ReedTheme { - ImpressionGuideBottomSheet( - onDismissRequest = {}, - sheetState = sheetState, - impressionState = TextFieldState(), - impressionGuideList = impressionGuideList, - beforeSelectedImpressionGuide = "", - selectedImpressionGuide = "", - onGuideClick = {}, - onCloseButtonClick = {}, - onSelectionConfirmButtonClick = {}, - ) - } -} diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt deleted file mode 100644 index 9481b360e..000000000 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.ninecraft.booket.feature.record.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.common.extensions.noRippleClickable -import com.ninecraft.booket.core.designsystem.ComponentPreview -import com.ninecraft.booket.core.designsystem.theme.Blank -import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.core.designsystem.theme.White -import com.ninecraft.booket.feature.record.R -import com.skydoves.compose.stability.runtime.TraceRecomposition - -@TraceRecomposition -@Composable -fun ImpressionGuideBox( - onClick: () -> Unit, - impressionText: String, - modifier: Modifier = Modifier, - isSelected: Boolean = false, -) { - val bgColor = if (isSelected) ReedTheme.colors.bgTertiary else White - val borderColor = if (isSelected) ReedTheme.colors.borderBrand else ReedTheme.colors.borderPrimary - val cornerShape = RoundedCornerShape(ReedTheme.radius.sm) - - Box( - modifier = modifier - .fillMaxWidth() - .clip(cornerShape) - .background(bgColor) - .border( - width = 1.dp, - color = borderColor, - shape = cornerShape, - ) - .noRippleClickable { - onClick() - } - .padding( - horizontal = ReedTheme.spacing.spacing4, - vertical = ReedTheme.spacing.spacing4, - ), - ) { - Row(verticalAlignment = Alignment.Bottom) { - Text( - text = stringResource(R.string.impression_guide_blank), - color = Blank, - style = ReedTheme.typography.label1SemiBold, - ) - Text( - text = impressionText, - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.label1SemiBold, - ) - } - } -} - -@ComponentPreview -@Composable -private fun ImpressionGuideBoxPreview() { - ReedTheme { - ImpressionGuideBox( - onClick = {}, - impressionText = "에서 위로 받았다", - ) - } -} diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt index 2f9e225d5..830679b89 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt @@ -3,18 +3,20 @@ package com.ninecraft.booket.feature.record.register import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.text.TextRange import com.ninecraft.booket.core.common.analytics.AnalyticsHelper import com.ninecraft.booket.core.common.utils.handleException +import com.ninecraft.booket.core.data.api.repository.EmotionRepository import com.ninecraft.booket.core.data.api.repository.RecordRepository import com.ninecraft.booket.core.designsystem.RecordStep -import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.EmotionGroupModel import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.OcrScreen import com.ninecraft.booket.feature.screens.RecordDetailScreen @@ -43,6 +45,7 @@ class RecordRegisterPresenter( @Assisted private val screen: RecordScreen, @Assisted private val navigator: Navigator, private val repository: RecordRepository, + private val emotionRepository: EmotionRepository, private val analyticsHelper: AnalyticsHelper, ) : Presenter { @@ -56,8 +59,6 @@ class RecordRegisterPresenter( private const val MAX_PAGE = 4032 private const val RECORD_INPUT_SENTENCE = "record_input_sentence" private const val RECORD_SELECT_EMOTION = "record_select_emotion" - private const val RECORD_INPUT_OPINION = "record_input_opinion" - private const val RECORD_INPUT_HELP = "record_input_help" private const val RECORD_COMPLETE = "record_complete" private const val RECORD_DETAIL = "record_detail" private const val ERROR_RECORD_SAVE = "error_record_save" @@ -65,44 +66,24 @@ class RecordRegisterPresenter( @Composable override fun present(): RecordRegisterUiState { - /** 2차 고도화 삭제 예정 ===================================================================== */ - val impressionState = rememberTextFieldState() - val impressionGuideList by rememberRetained { - mutableStateOf( - listOf( - "에서 위로 받았다", - "이 마음에 남았다", - "에서 작가의 의도가 궁금하다", - "에 대한 다른 사람들의 생각이 궁금하다", - "에서 크게 공감이 된다", - "을 보고 예전 기억이 났다", - "에서 문장에 머물렀다", - ).toPersistentList(), - ) - } - var selectedImpressionGuide by rememberRetained { mutableStateOf("") } - var beforeSelectedImpressionGuide by rememberRetained { mutableStateOf(selectedImpressionGuide) } - var isImpressionGuideBottomSheetVisible by rememberRetained { mutableStateOf(false) } - var isScanTooltipVisible by rememberRetained { mutableStateOf(true) } - var isImpressionGuideTooltipVisible by rememberRetained { mutableStateOf(true) } - - /** ====================================================================================== */ val scope = rememberCoroutineScope() var isLoading by rememberRetained { mutableStateOf(false) } + var emotionUiState by rememberRetained { mutableStateOf(EmotionUiState.Idle) } var sideEffect by rememberRetained { mutableStateOf(null) } var currentStep by rememberRetained { mutableStateOf(RecordStep.QUOTE) } val recordPageState = rememberTextFieldState() val recordSentenceState = rememberTextFieldState() val memoState = rememberTextFieldState() - val emotions by rememberRetained { mutableStateOf(Emotion.entries.toPersistentList()) } - var emotionDetails by rememberRetained { mutableStateOf(persistentListOf()) } - var selectedEmotion by rememberRetained { mutableStateOf(null) } - var selectedEmotionDetails by rememberRetained { mutableStateOf>>(persistentMapOf()) } - var committedEmotion by rememberRetained { mutableStateOf(null) } - var committedEmotionDetails by rememberRetained { mutableStateOf>>(persistentMapOf()) } + var emotionGroups by rememberRetained { mutableStateOf(persistentListOf()) } + var pendingEmotionCode by rememberRetained { mutableStateOf(null) } + var selectedEmotionCode by rememberRetained { mutableStateOf(null) } + var selectedEmotionMap by rememberRetained { mutableStateOf>>(persistentMapOf()) } + var committedEmotionCode by rememberRetained { mutableStateOf(null) } + var committedEmotionMap by rememberRetained { mutableStateOf>>(persistentMapOf()) } var isEmotionDetailBottomSheetVisible by rememberRetained { mutableStateOf(false) } var savedRecordId by rememberRetained { mutableStateOf("") } var isExitDialogVisible by rememberRetained { mutableStateOf(false) } + var isEmotionEditDialogVisible by rememberRetained { mutableStateOf(false) } var isRecordSavedDialogVisible by rememberRetained { mutableStateOf(false) } val isPageError by remember { derivedStateOf { @@ -118,10 +99,8 @@ class RecordRegisterPresenter( } RecordStep.EMOTION -> { - committedEmotion != null + committedEmotionCode != null } - - RecordStep.IMPRESSION -> true } } } @@ -135,10 +114,11 @@ class RecordRegisterPresenter( fun postRecord( userBookId: String, - pageNumber: Int, + pageNumber: Int?, quote: String, - emotionTags: List, - impression: String, + primaryEmotion: String, + detailEmotionTagIds: List, + review: String, ) { scope.launch { try { @@ -147,8 +127,9 @@ class RecordRegisterPresenter( userBookId = userBookId, pageNumber = pageNumber, quote = quote, - emotionTags = emotionTags, - review = impression, + review = review, + primaryEmotion = primaryEmotion, + detailEmotionTagIds = detailEmotionTagIds, ).onSuccess { result -> analyticsHelper.logEvent(RECORD_COMPLETE) savedRecordId = result.id @@ -174,17 +155,29 @@ class RecordRegisterPresenter( } } - fun provideEmotionDetailMap(): Map> { - return mapOf( - Emotion.WARM to persistentListOf("위로받은", "포근한", "다정한", "고마운", "마음이 놓이는", "편안한"), - Emotion.JOY to persistentListOf("설레는", "뿌듯한", "유쾌한", "기쁜", "흥미진진한"), - Emotion.SAD to persistentListOf("허무함", "외로운", "아쉬운", "먹먹한", "애틋한", "안타까운", "그리운"), - Emotion.INSIGHT to persistentListOf("감탄한", "통찰력을 얻은", "영감을 받은", "생각이 깊어진", "새롭게 이해한"), - ) - } + fun getEmotionGroups() { + scope.launch { + emotionUiState = EmotionUiState.Loading + emotionRepository.getEmotions() + .onSuccess { result -> + emotionUiState = EmotionUiState.Success + emotionGroups = result.emotions.toPersistentList() + }.onFailure { exception -> + emotionUiState = EmotionUiState.Error(exception) - fun getEmotionDetails(emotion: Emotion): ImmutableList { - return provideEmotionDetailMap()[emotion] ?: persistentListOf() + val handleErrorMessage = { message: String -> + Logger.e(message) + } + + handleException( + exception = exception, + onError = handleErrorMessage, + onLoginRequired = { + navigator.resetRoot(LoginScreen()) + }, + ) + } + } } fun handleEvent(event: RecordRegisterUiEvent) { @@ -198,10 +191,6 @@ class RecordRegisterPresenter( RecordStep.EMOTION -> { currentStep = RecordStep.QUOTE } - - RecordStep.IMPRESSION -> { - currentStep = RecordStep.EMOTION - } } } @@ -221,56 +210,62 @@ class RecordRegisterPresenter( } is RecordRegisterUiEvent.OnSentenceScanButtonClick -> { - isScanTooltipVisible = false ocrNavigator.goTo(OcrScreen) } - is RecordRegisterUiEvent.OnSelectEmotion -> { - selectedEmotion = event.emotion - } - - is RecordRegisterUiEvent.OnSelectEmotionV2 -> { - selectedEmotion = event.emotion - emotionDetails = getEmotionDetails(event.emotion).toPersistentList() - isEmotionDetailBottomSheetVisible = true + is RecordRegisterUiEvent.OnSelectEmotionCode -> { + if (selectedEmotionCode != null && selectedEmotionCode != event.emotionCode) { + pendingEmotionCode = event.emotionCode + isEmotionEditDialogVisible = true + } else { + selectedEmotionCode = event.emotionCode + + if (selectedEmotionCode == EmotionCode.OTHER) { + committedEmotionCode = selectedEmotionCode + committedEmotionMap = persistentMapOf() + selectedEmotionMap = persistentMapOf() + } else { + isEmotionDetailBottomSheetVisible = true + } + } } is RecordRegisterUiEvent.OnEmotionDetailToggled -> { - val emotionKey = selectedEmotion ?: return - val currentDetails = selectedEmotionDetails[selectedEmotion].orEmpty() - val updatedDetails = if (event.detail in currentDetails) { - currentDetails - event.detail + val emotionKey = selectedEmotionCode ?: return + val currentDetails = selectedEmotionMap[selectedEmotionCode].orEmpty() + val updatedDetails = if (event.detailId in currentDetails) { + currentDetails - event.detailId } else { - currentDetails + event.detail + currentDetails + event.detailId } - selectedEmotionDetails = selectedEmotionDetails.put(emotionKey, updatedDetails.toPersistentList()) + selectedEmotionMap = selectedEmotionMap.put(emotionKey, updatedDetails.toPersistentList()) } is RecordRegisterUiEvent.OnEmotionDetailRemoved -> { - val emotionKey = selectedEmotion ?: return - val currentDetails = committedEmotionDetails[selectedEmotion].orEmpty() - val updatedDetails = currentDetails - event.detail + val emotionKey = selectedEmotionCode ?: return + val currentDetails = committedEmotionMap[selectedEmotionCode].orEmpty() + val updatedDetails = currentDetails - event.detailId - committedEmotionDetails = committedEmotionDetails.put(emotionKey, updatedDetails.toPersistentList()) - selectedEmotionDetails = selectedEmotionDetails.put(emotionKey, updatedDetails.toPersistentList()) + committedEmotionMap = committedEmotionMap.put(emotionKey, updatedDetails.toPersistentList()) + selectedEmotionMap = selectedEmotionMap.put(emotionKey, updatedDetails.toPersistentList()) } is RecordRegisterUiEvent.OnEmotionDetailSkipped -> { - committedEmotion = selectedEmotion + committedEmotionCode = selectedEmotionCode // 건너뛰기 시 세부감정 선택 초기화 - committedEmotionDetails = persistentMapOf() - selectedEmotionDetails = persistentMapOf() + committedEmotionMap = persistentMapOf() + selectedEmotionMap = persistentMapOf() isEmotionDetailBottomSheetVisible = false } is RecordRegisterUiEvent.OnEmotionDetailCommitted -> { - val emotionKey = selectedEmotion ?: return - val details = selectedEmotionDetails[emotionKey] ?: persistentListOf() + val emotionKey = selectedEmotionCode ?: return + val details = selectedEmotionMap[emotionKey] ?: persistentListOf() - committedEmotion = emotionKey - committedEmotionDetails = persistentMapOf(emotionKey to details) - selectedEmotionDetails = persistentMapOf(emotionKey to details) + committedEmotionCode = emotionKey + committedEmotionMap = persistentMapOf(emotionKey to details) + selectedEmotionMap = persistentMapOf(emotionKey to details) isEmotionDetailBottomSheetVisible = false } @@ -278,51 +273,6 @@ class RecordRegisterPresenter( isEmotionDetailBottomSheetVisible = false } - /** 2차 고도화 삭제 예정 ===================================================================== */ - is RecordRegisterUiEvent.OnImpressionGuideButtonClick -> { - analyticsHelper.logScreenView(RECORD_INPUT_HELP) - isImpressionGuideTooltipVisible = false - beforeSelectedImpressionGuide = selectedImpressionGuide - if (impressionState.text.isEmpty()) { - selectedImpressionGuide = "" - } - isImpressionGuideBottomSheetVisible = true - } - - is RecordRegisterUiEvent.OnSelectImpressionGuide -> { - val index = event.index - if (index in impressionGuideList.indices) { - selectedImpressionGuide = impressionGuideList[index] - } - } - - is RecordRegisterUiEvent.OnImpressionGuideConfirmed -> { - val currentImpressionText = impressionState.text.toString() - - if (currentImpressionText.isNotEmpty()) { - // 이미 작성된 감상문이 있는 경우 줄바꿈해서 추가 - val startIndex = currentImpressionText.length - - impressionState.edit { - replace(0, length, currentImpressionText + "\n" + selectedImpressionGuide) - this.selection = TextRange(startIndex + 1) // 줄바꿈한 문장 맨 앞에 커서 위치 - } - } else { - impressionState.edit { - replace(0, length, "") - append(selectedImpressionGuide) - this.selection = TextRange(0) // 커서를 문장 맨 앞에 위치 - } - } - - isImpressionGuideBottomSheetVisible = false - } - - is RecordRegisterUiEvent.OnImpressionGuideBottomSheetDismiss -> { - isImpressionGuideBottomSheetVisible = false - } - /** ====================================================================================== */ - is RecordRegisterUiEvent.OnNextButtonClick -> { when (currentStep) { RecordStep.QUOTE -> { @@ -330,16 +280,13 @@ class RecordRegisterPresenter( } RecordStep.EMOTION -> { - currentStep = RecordStep.IMPRESSION - } - - RecordStep.IMPRESSION -> { postRecord( userBookId = screen.userBookId, - pageNumber = recordPageState.text.toString().toIntOrNull() ?: 0, + pageNumber = recordPageState.text.toString().toIntOrNull(), quote = recordSentenceState.text.toString(), - emotionTags = selectedEmotion?.let { listOf(it.displayName) } ?: emptyList(), - impression = impressionState.text.toString(), + review = memoState.text.toString(), + primaryEmotion = committedEmotionCode?.name ?: "", + detailEmotionTagIds = committedEmotionMap[committedEmotionCode] ?: persistentListOf(), ) } } @@ -358,43 +305,61 @@ class RecordRegisterPresenter( navigator.delayedPop() } } + + RecordRegisterUiEvent.OnRetryGetEmotions -> { + getEmotionGroups() + } + + RecordRegisterUiEvent.OnEmotionEditDialogConfirm -> { + selectedEmotionCode = pendingEmotionCode + + if (selectedEmotionCode == EmotionCode.OTHER) { + committedEmotionCode = selectedEmotionCode + committedEmotionMap = persistentMapOf() + selectedEmotionMap = persistentMapOf() + } else { + isEmotionDetailBottomSheetVisible = true + } + isEmotionEditDialogVisible = false + } + + RecordRegisterUiEvent.OnEmotionEditDialogDismiss -> { + isEmotionEditDialogVisible = false + } } } + LaunchedEffect(Unit) { + getEmotionGroups() + } + ImpressionEffect(currentStep) { val screenName = when (currentStep) { RecordStep.QUOTE -> RECORD_INPUT_SENTENCE RecordStep.EMOTION -> RECORD_SELECT_EMOTION - RecordStep.IMPRESSION -> RECORD_INPUT_OPINION } analyticsHelper.logScreenView(screenName) } return RecordRegisterUiState( isLoading = isLoading, + emotionUiState = emotionUiState, currentStep = currentStep, recordPageState = recordPageState, recordSentenceState = recordSentenceState, memoState = memoState, isPageError = isPageError, - emotions = emotions, - emotionDetails = emotionDetails, - selectedEmotion = selectedEmotion, - selectedEmotionDetails = selectedEmotionDetails, - committedEmotion = committedEmotion, - committedEmotionDetails = committedEmotionDetails, + emotionGroups = emotionGroups, + selectedEmotionCode = selectedEmotionCode, + selectedEmotionMap = selectedEmotionMap, + committedEmotionCode = committedEmotionCode, + committedEmotionMap = committedEmotionMap, isEmotionDetailBottomSheetVisible = isEmotionDetailBottomSheetVisible, - impressionState = impressionState, - impressionGuideList = impressionGuideList, - selectedImpressionGuide = selectedImpressionGuide, - beforeSelectedImpressionGuide = beforeSelectedImpressionGuide, savedRecordId = savedRecordId, isNextButtonEnabled = isNextButtonEnabled, - isImpressionGuideBottomSheetVisible = isImpressionGuideBottomSheetVisible, isExitDialogVisible = isExitDialogVisible, + isEmotionEditDialogVisible = isEmotionEditDialogVisible, isRecordSavedDialogVisible = isRecordSavedDialogVisible, - isScanTooltipVisible = isScanTooltipVisible, - isImpressionGuideTooltipVisible = isImpressionGuideTooltipVisible, sideEffect = sideEffect, eventSink = ::handleEvent, ) diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt index 50e67d41f..430a4998f 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.common.extensions.preventMultiTouch +import com.ninecraft.booket.core.common.extensions.toErrorType import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.RecordStep import com.ninecraft.booket.core.designsystem.component.RecordProgressBar @@ -25,10 +26,10 @@ import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White import com.ninecraft.booket.core.ui.component.ReedBackTopAppBar import com.ninecraft.booket.core.ui.component.ReedDialog +import com.ninecraft.booket.core.ui.component.ReedErrorUi import com.ninecraft.booket.core.ui.component.ReedLoadingIndicator import com.ninecraft.booket.feature.record.R import com.ninecraft.booket.feature.record.step.EmotionStep -import com.ninecraft.booket.feature.record.step.ImpressionStep import com.ninecraft.booket.feature.record.step.QuoteStep import com.ninecraft.booket.feature.screens.RecordScreen import com.skydoves.compose.stability.runtime.TraceRecomposition @@ -76,11 +77,23 @@ internal fun RecordRegisterUi( } RecordStep.EMOTION -> { - EmotionStep(state = state) - } + when (state.emotionUiState) { + is EmotionUiState.Idle -> {} + is EmotionUiState.Loading -> { + ReedLoadingIndicator() + } + + is EmotionUiState.Success -> { + EmotionStep(state = state) + } - RecordStep.IMPRESSION -> { - ImpressionStep(state = state) + is EmotionUiState.Error -> { + ReedErrorUi( + errorType = state.emotionUiState.exception.toErrorType(), + onRetryClick = { state.eventSink(RecordRegisterUiEvent.OnRetryGetEmotions) }, + ) + } + } } } } @@ -94,8 +107,8 @@ internal fun RecordRegisterUi( ReedDialog( title = stringResource(R.string.record_exit_dialog_title), description = stringResource(R.string.record_exit_dialog_description), - confirmButtonText = stringResource(R.string.record_exit_dialog_confirm), - dismissButtonText = stringResource(R.string.record_exit_dialog_dismiss), + confirmButtonText = stringResource(R.string.record_dialog_confirm), + dismissButtonText = stringResource(R.string.record_dialog_dismiss), onConfirmRequest = { state.eventSink(RecordRegisterUiEvent.OnExitDialogConfirm) }, @@ -105,6 +118,21 @@ internal fun RecordRegisterUi( ) } + if (state.isEmotionEditDialogVisible) { + ReedDialog( + title = stringResource(R.string.emotion_edit_dialog_title), + description = stringResource(R.string.emotion_edit_dialog_description), + confirmButtonText = stringResource(R.string.record_dialog_confirm), + dismissButtonText = stringResource(R.string.record_dialog_dismiss), + onConfirmRequest = { + state.eventSink(RecordRegisterUiEvent.OnEmotionEditDialogConfirm) + }, + onDismissRequest = { + state.eventSink(RecordRegisterUiEvent.OnEmotionEditDialogDismiss) + }, + ) + } + if (state.isRecordSavedDialogVisible) { ReedDialog( title = stringResource(R.string.record_saved_dialog_title), diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt index 8989a371f..36f723d4b 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt @@ -3,7 +3,8 @@ package com.ninecraft.booket.feature.record.register import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.Immutable import com.ninecraft.booket.core.designsystem.RecordStep -import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.EmotionGroupModel import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import kotlinx.collections.immutable.ImmutableList @@ -12,31 +13,33 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import java.util.UUID +@Immutable +sealed interface EmotionUiState { + data object Idle : EmotionUiState + data object Loading : EmotionUiState + data object Success : EmotionUiState + data class Error(val exception: Throwable) : EmotionUiState +} + data class RecordRegisterUiState( val isLoading: Boolean = false, + val emotionUiState: EmotionUiState = EmotionUiState.Idle, val currentStep: RecordStep = RecordStep.QUOTE, val recordPageState: TextFieldState = TextFieldState(), val recordSentenceState: TextFieldState = TextFieldState(), val isPageError: Boolean = false, val memoState: TextFieldState = TextFieldState(), - val emotions: ImmutableList = persistentListOf(), - val emotionDetails: ImmutableList = persistentListOf(), - val selectedEmotion: Emotion? = null, - val selectedEmotionDetails: PersistentMap> = persistentMapOf(), - val committedEmotion: Emotion? = null, - val committedEmotionDetails: PersistentMap> = persistentMapOf(), + val emotionGroups: ImmutableList = persistentListOf(), + val selectedEmotionCode: EmotionCode? = null, + val selectedEmotionMap: PersistentMap> = persistentMapOf(), + val committedEmotionCode: EmotionCode? = null, + val committedEmotionMap: PersistentMap> = persistentMapOf(), val isEmotionDetailBottomSheetVisible: Boolean = false, - val impressionState: TextFieldState = TextFieldState(), - val impressionGuideList: ImmutableList = persistentListOf(), - val selectedImpressionGuide: String = "", - val beforeSelectedImpressionGuide: String = "", val savedRecordId: String = "", val isNextButtonEnabled: Boolean = false, - val isImpressionGuideBottomSheetVisible: Boolean = false, val isExitDialogVisible: Boolean = false, + val isEmotionEditDialogVisible: Boolean = false, val isRecordSavedDialogVisible: Boolean = false, - val isScanTooltipVisible: Boolean = true, - val isImpressionGuideTooltipVisible: Boolean = true, val sideEffect: RecordRegisterSideEffect? = null, val eventSink: (RecordRegisterUiEvent) -> Unit, ) : CircuitUiState @@ -54,19 +57,17 @@ sealed interface RecordRegisterUiEvent : CircuitUiEvent { data object OnClearClick : RecordRegisterUiEvent data object OnNextButtonClick : RecordRegisterUiEvent data object OnSentenceScanButtonClick : RecordRegisterUiEvent - data class OnSelectEmotion(val emotion: Emotion) : RecordRegisterUiEvent - data class OnSelectEmotionV2(val emotion: Emotion) : RecordRegisterUiEvent - data class OnEmotionDetailToggled(val detail: String) : RecordRegisterUiEvent - data class OnEmotionDetailRemoved(val detail: String) : RecordRegisterUiEvent + data class OnSelectEmotionCode(val emotionCode: EmotionCode) : RecordRegisterUiEvent + data class OnEmotionDetailToggled(val detailId: String) : RecordRegisterUiEvent + data class OnEmotionDetailRemoved(val detailId: String) : RecordRegisterUiEvent data object OnEmotionDetailSkipped : RecordRegisterUiEvent data object OnEmotionDetailCommitted : RecordRegisterUiEvent data object OnEmotionDetailBottomSheetDismiss : RecordRegisterUiEvent - data object OnImpressionGuideButtonClick : RecordRegisterUiEvent - data object OnImpressionGuideBottomSheetDismiss : RecordRegisterUiEvent - data class OnSelectImpressionGuide(val index: Int) : RecordRegisterUiEvent - data object OnImpressionGuideConfirmed : RecordRegisterUiEvent data object OnExitDialogConfirm : RecordRegisterUiEvent data object OnExitDialogDismiss : RecordRegisterUiEvent + data object OnEmotionEditDialogConfirm : RecordRegisterUiEvent + data object OnEmotionEditDialogDismiss : RecordRegisterUiEvent data class OnRecordSavedDialogConfirm(val recordId: String) : RecordRegisterUiEvent data object OnRecordSavedDialogDismiss : RecordRegisterUiEvent + data object OnRetryGetEmotions : RecordRegisterUiEvent } diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionDetailBottomSheet.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionDetailBottomSheet.kt similarity index 77% rename from feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionDetailBottomSheet.kt rename to feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionDetailBottomSheet.kt index 19994c8a5..c69e56f1d 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionDetailBottomSheet.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionDetailBottomSheet.kt @@ -1,4 +1,4 @@ -package com.ninecraft.booket.feature.record.step_v2 +package com.ninecraft.booket.feature.record.step import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -29,7 +29,9 @@ import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle import com.ninecraft.booket.core.designsystem.component.chip.ReedSelectableChip import com.ninecraft.booket.core.designsystem.component.chip.mediumChipStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.core.model.DetailEmotionModel +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.EmotionGroupModel import com.ninecraft.booket.core.ui.component.ReedBottomSheet import com.ninecraft.booket.feature.record.R import kotlinx.collections.immutable.ImmutableList @@ -39,9 +41,8 @@ import com.ninecraft.booket.core.designsystem.R as designR @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun EmotionDetailBottomSheet( - emotion: Emotion, - emotionDetails: ImmutableList, - selectedEmotionDetail: ImmutableList, + emotionGroup: EmotionGroupModel, + selectedEmotionDetailIds: ImmutableList, onDismissRequest: () -> Unit, sheetState: SheetState, onCloseButtonClick: () -> Unit, @@ -49,8 +50,6 @@ internal fun EmotionDetailBottomSheet( onSkipButtonClick: () -> Unit, onConfirmButtonClick: () -> Unit, ) { - val emotionCategoryName = "'${emotion.displayName}'" - ReedBottomSheet( onDismissRequest = { onDismissRequest() @@ -71,7 +70,7 @@ internal fun EmotionDetailBottomSheet( horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = stringResource(R.string.emotion_detail_title, emotionCategoryName), + text = stringResource(R.string.emotion_detail_title, emotionGroup.displayName), color = ReedTheme.colors.contentPrimary, textAlign = TextAlign.Center, style = ReedTheme.typography.heading2SemiBold, @@ -106,13 +105,13 @@ internal fun EmotionDetailBottomSheet( ), verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), ) { - emotionDetails.forEach { detail -> + emotionGroup.detailEmotions.forEach { detail -> ReedSelectableChip( - label = detail, + label = detail.name, chipSizeStyle = mediumChipStyle, - selected = detail in selectedEmotionDetail, + selected = detail.id in selectedEmotionDetailIds, onClick = { - onEmotionDetailToggled(detail) + onEmotionDetailToggled(detail.id) }, ) } @@ -141,7 +140,7 @@ internal fun EmotionDetailBottomSheet( sizeStyle = largeButtonStyle, colorStyle = ReedButtonColorStyle.PRIMARY, modifier = Modifier.weight(1f), - enabled = selectedEmotionDetail.isNotEmpty(), + enabled = selectedEmotionDetailIds.isNotEmpty(), ) } } @@ -152,8 +151,36 @@ internal fun EmotionDetailBottomSheet( @ComponentPreview @Composable private fun EmotionDetailBottomSheetPreview() { - val emotionDetails = persistentListOf("위로받은", "포근한", "다정한", "고마운", "마음이 놓이는", "편안한") - + val warmthEmotionGroup = EmotionGroupModel( + code = EmotionCode.WARMTH, + displayName = "따뜻함", + detailEmotions = persistentListOf( + DetailEmotionModel( + id = "84f95d93-e54c-11f0-8545-525ae7dd628c", + name = "위로받은", + ), + DetailEmotionModel( + id = "84f95e7e-e54c-11f0-8545-525ae7dd628c", + name = "포근한", + ), + DetailEmotionModel( + id = "84f95f13-e54c-11f0-8545-525ae7dd628c", + name = "다정한", + ), + DetailEmotionModel( + id = "84f95fc0-e54c-11f0-8545-525ae7dd628c", + name = "고마운", + ), + DetailEmotionModel( + id = "84f96094-e54c-11f0-8545-525ae7dd628c", + name = "마음이 놓이는", + ), + DetailEmotionModel( + id = "84f9612c-e54c-11f0-8545-525ae7dd628c", + name = "편안한", + ), + ), + ) val sheetState = SheetState( skipPartiallyExpanded = true, initialValue = SheetValue.Expanded, @@ -162,9 +189,8 @@ private fun EmotionDetailBottomSheetPreview() { ) ReedTheme { EmotionDetailBottomSheet( - emotion = Emotion.WARM, - emotionDetails = emotionDetails, - selectedEmotionDetail = persistentListOf(), + emotionGroup = warmthEmotionGroup, + selectedEmotionDetailIds = persistentListOf(), onDismissRequest = {}, sheetState = sheetState, onCloseButtonClick = {}, diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionItem.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionItem.kt similarity index 65% rename from feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionItem.kt rename to feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionItem.kt index 022e66ead..bebb67dd3 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionItem.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionItem.kt @@ -1,4 +1,4 @@ -package com.ninecraft.booket.feature.record.step_v2 +package com.ninecraft.booket.feature.record.step import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -32,16 +32,18 @@ import com.ninecraft.booket.core.designsystem.R import com.ninecraft.booket.core.designsystem.component.chip.ReedRemovableChip import com.ninecraft.booket.core.designsystem.component.chip.smallChipStyle import com.ninecraft.booket.core.designsystem.descriptionRes -import com.ninecraft.booket.core.designsystem.graphicResV2 +import com.ninecraft.booket.core.designsystem.categoryGraphicRes import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.core.model.DetailEmotionModel +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.EmotionGroupModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @Composable internal fun EmotionItem( - emotion: Emotion, - selectedEmotionDetails: ImmutableList, + emotionGroup: EmotionGroupModel, + selectedEmotionDetailIds: ImmutableList, onClick: () -> Unit, isSelected: Boolean, onEmotionDetailRemove: (String) -> Unit, @@ -73,24 +75,26 @@ internal fun EmotionItem( ), ) { Row(verticalAlignment = Alignment.CenterVertically) { - if (emotion.graphicResV2 != null) { + val emotionGraphicRes = emotionGroup.code.categoryGraphicRes + if (emotionGraphicRes != null) { Image( - painter = painterResource(emotion.graphicResV2!!), + painter = painterResource(emotionGraphicRes), contentDescription = "Emotion Image", modifier = Modifier .size(60.dp) .clip(CircleShape), ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4)) } - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4)) + Column { Text( - text = emotion.displayName, + text = emotionGroup.displayName, color = ReedTheme.colors.contentPrimary, style = ReedTheme.typography.headline1SemiBold, ) Text( - text = stringResource(emotion.descriptionRes), + text = stringResource(emotionGroup.code.descriptionRes), color = ReedTheme.colors.contentTertiary, style = ReedTheme.typography.label1Medium, ) @@ -103,19 +107,20 @@ internal fun EmotionItem( ) } - if (selectedEmotionDetails.isNotEmpty()) { + if (selectedEmotionDetailIds.isNotEmpty()) { Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) FlowRow( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), ) { - selectedEmotionDetails.forEach { detail -> + selectedEmotionDetailIds.forEach { detailId -> + val detailName = emotionGroup.detailEmotions.firstOrNull { it.id == detailId }?.name ?: return@forEach ReedRemovableChip( - label = detail, + label = detailName, chipSizeStyle = smallChipStyle, onRemove = { - onEmotionDetailRemove(detail) + onEmotionDetailRemove(detailId) }, ) } @@ -127,12 +132,46 @@ internal fun EmotionItem( @ComponentPreview @Composable private fun EmotionItemPreview() { - val selectedEmotionDetails = persistentListOf("위로받은", "포근한", "다정한", "고마운", "마음이 놓이는", "편안한") + val warmthEmotionGroup = EmotionGroupModel( + code = EmotionCode.WARMTH, + displayName = "따뜻함", + detailEmotions = persistentListOf( + DetailEmotionModel( + id = "84f95d93-e54c-11f0-8545-525ae7dd628c", + name = "위로받은", + ), + DetailEmotionModel( + id = "84f95e7e-e54c-11f0-8545-525ae7dd628c", + name = "포근한", + ), + DetailEmotionModel( + id = "84f95f13-e54c-11f0-8545-525ae7dd628c", + name = "다정한", + ), + DetailEmotionModel( + id = "84f95fc0-e54c-11f0-8545-525ae7dd628c", + name = "고마운", + ), + DetailEmotionModel( + id = "84f96094-e54c-11f0-8545-525ae7dd628c", + name = "마음이 놓이는", + ), + DetailEmotionModel( + id = "84f9612c-e54c-11f0-8545-525ae7dd628c", + name = "편안한", + ), + ), + ) + + val selectedEmotionDetailIds = persistentListOf( + "84f95fc0-e54c-11f0-8545-525ae7dd628c", + "84f96094-e54c-11f0-8545-525ae7dd628c", + ) ReedTheme { EmotionItem( - emotion = Emotion.WARM, - selectedEmotionDetails = selectedEmotionDetails, + emotionGroup = warmthEmotionGroup, + selectedEmotionDetailIds = selectedEmotionDetailIds, onClick = {}, isSelected = false, onEmotionDetailRemove = {}, diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt index 0561539e5..4e96499de 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/EmotionStep.kt @@ -1,11 +1,7 @@ package com.ninecraft.booket.feature.record.step -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -13,39 +9,40 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.common.extensions.clickableSingle import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle -import com.ninecraft.booket.core.designsystem.graphicRes import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White -import com.ninecraft.booket.core.model.Emotion +import com.ninecraft.booket.core.model.DetailEmotionModel +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.core.model.EmotionGroupModel import com.ninecraft.booket.feature.record.R import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent import com.ninecraft.booket.feature.record.register.RecordRegisterUiState import com.skydoves.compose.stability.runtime.TraceRecomposition -import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @TraceRecomposition @Composable -fun EmotionStep( +internal fun EmotionStep( state: RecordRegisterUiState, modifier: Modifier = Modifier, ) { - val emotionPairs = remember(state.emotions) { state.emotions.chunked(2) } + val emotionDetailBottomSheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() Box( modifier = modifier @@ -76,29 +73,22 @@ fun EmotionStep( ) } item { - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing6)) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) } - items(emotionPairs) { pair -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing3), - ) { - pair.forEach { tag -> - EmotionItem( - emotion = tag, - onClick = { - state.eventSink(RecordRegisterUiEvent.OnSelectEmotion(tag)) - }, - isSelected = state.selectedEmotion == tag, - modifier = Modifier.weight(1f), - ) - } - if (pair.size == 1) { - Spacer(modifier = Modifier.weight(1f)) - } - } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) + items(state.emotionGroups) { emotion -> + EmotionItem( + emotionGroup = emotion, + selectedEmotionDetailIds = state.committedEmotionMap[emotion.code] ?: persistentListOf(), + onClick = { + state.eventSink(RecordRegisterUiEvent.OnSelectEmotionCode(emotion.code)) + }, + isSelected = state.committedEmotionCode == emotion.code, + onEmotionDetailRemove = { detail -> + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailRemoved(detail)) + }, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) } } @@ -114,57 +104,82 @@ fun EmotionStep( .padding(horizontal = ReedTheme.spacing.spacing5) .padding(bottom = ReedTheme.spacing.spacing4), enabled = state.isNextButtonEnabled, - text = stringResource(R.string.record_next_button_text), + text = stringResource(R.string.record_finish_button_text), multipleEventsCutterEnabled = false, ) } -} - -@Composable -private fun EmotionItem( - emotion: Emotion, - onClick: () -> Unit, - isSelected: Boolean, - modifier: Modifier = Modifier, -) { - val cornerShape = RoundedCornerShape(ReedTheme.radius.md) - Box( - modifier = modifier - .height(214.dp) - .clip(cornerShape) - .background(color = ReedTheme.colors.bgTertiary) - .then( - if (isSelected) Modifier.border( - width = ReedTheme.border.border15, - color = ReedTheme.colors.borderBrand, - shape = cornerShape, - ) - else Modifier, - ) - .clickableSingle { - onClick() + if (state.isEmotionDetailBottomSheetVisible) { + val selectedEmotionGroup = state.emotionGroups.firstOrNull { it.code == state.selectedEmotionCode } ?: return + EmotionDetailBottomSheet( + emotionGroup = selectedEmotionGroup, + selectedEmotionDetailIds = state.selectedEmotionMap[state.selectedEmotionCode] ?: persistentListOf(), + onDismissRequest = { + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailBottomSheetDismiss) + }, + sheetState = emotionDetailBottomSheetState, + onCloseButtonClick = { + coroutineScope.launch { + emotionDetailBottomSheetState.hide() + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailBottomSheetDismiss) + } + }, + onEmotionDetailToggled = { detail -> + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailToggled(detail)) + }, + onSkipButtonClick = { + coroutineScope.launch { + emotionDetailBottomSheetState.hide() + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailSkipped) + } + }, + onConfirmButtonClick = { + coroutineScope.launch { + emotionDetailBottomSheetState.hide() + state.eventSink(RecordRegisterUiEvent.OnEmotionDetailCommitted) + } }, - contentAlignment = Alignment.Center, - ) { - Image( - painter = painterResource(emotion.graphicRes), - contentDescription = "Emotion Image", - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Crop, ) } } @ComponentPreview @Composable -private fun RecordRegisterPreview() { - val emotions = Emotion.entries.toPersistentList() - +private fun EmotionStepPreview() { + val warmthEmotionGroup = EmotionGroupModel( + code = EmotionCode.WARMTH, + displayName = "따뜻함", + detailEmotions = persistentListOf( + DetailEmotionModel( + id = "84f95d93-e54c-11f0-8545-525ae7dd628c", + name = "위로받은", + ), + DetailEmotionModel( + id = "84f95e7e-e54c-11f0-8545-525ae7dd628c", + name = "포근한", + ), + DetailEmotionModel( + id = "84f95f13-e54c-11f0-8545-525ae7dd628c", + name = "다정한", + ), + DetailEmotionModel( + id = "84f95fc0-e54c-11f0-8545-525ae7dd628c", + name = "고마운", + ), + DetailEmotionModel( + id = "84f96094-e54c-11f0-8545-525ae7dd628c", + name = "마음이 놓이는", + ), + DetailEmotionModel( + id = "84f9612c-e54c-11f0-8545-525ae7dd628c", + name = "편안한", + ), + ), + ) ReedTheme { EmotionStep( state = RecordRegisterUiState( - emotions = emotions, + emotionGroups = persistentListOf(warmthEmotionGroup), eventSink = {}, ), ) diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt deleted file mode 100644 index 016546ced..000000000 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt +++ /dev/null @@ -1,239 +0,0 @@ -package com.ninecraft.booket.feature.record.step - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.relocation.BringIntoViewRequester -import androidx.compose.foundation.relocation.bringIntoViewRequester -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.designsystem.ComponentPreview -import com.ninecraft.booket.core.designsystem.component.button.ReedButton -import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle -import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle -import com.ninecraft.booket.core.designsystem.component.button.smallRoundedButtonStyle -import com.ninecraft.booket.core.designsystem.component.textfield.ReedRecordTextField -import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.core.designsystem.theme.White -import com.ninecraft.booket.feature.record.R -import com.ninecraft.booket.feature.record.component.CustomTooltipBox -import com.ninecraft.booket.feature.record.component.ImpressionGuideBottomSheet -import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent -import com.ninecraft.booket.feature.record.register.RecordRegisterUiState -import com.skydoves.compose.stability.runtime.TraceRecomposition -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import tech.thdev.compose.extensions.keyboard.state.foundation.rememberKeyboardVisible -import com.ninecraft.booket.core.designsystem.R as designR - -@TraceRecomposition -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ImpressionStep( - state: RecordRegisterUiState, - modifier: Modifier = Modifier, -) { - val coroutineScope = rememberCoroutineScope() - val impressionGuideBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val focusRequester = remember { FocusRequester() } - val scrollState = rememberScrollState() - val bringIntoViewRequester = remember { BringIntoViewRequester() } - val keyboardState by rememberKeyboardVisible() - var isImpressionTextFieldFocused by remember { mutableStateOf(false) } - - LaunchedEffect(keyboardState, isImpressionTextFieldFocused) { - if (keyboardState && isImpressionTextFieldFocused) { - delay(150) - bringIntoViewRequester.bringIntoView() - } - } - - Column( - modifier = modifier - .fillMaxSize() - .background(color = White) - .imePadding(), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(horizontal = ReedTheme.spacing.spacing5) - .padding(bottom = 16.dp) - .verticalScroll(scrollState), - ) { - FlowRow( - itemVerticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = stringResource(R.string.impression_step_title), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.heading1Bold, - ) - Spacer(modifier = Modifier.width(10.dp)) - Box( - modifier = Modifier - .clip(RoundedCornerShape(ReedTheme.radius.xs)) - .background(ReedTheme.colors.bgTertiary), - ) { - Text( - text = stringResource(R.string.select), - modifier = Modifier.padding( - start = ReedTheme.spacing.spacing2, - top = ReedTheme.spacing.spacing05, - end = ReedTheme.spacing.spacing2, - bottom = ReedTheme.spacing.spacing05, - ), - color = ReedTheme.colors.contentBrand, - style = ReedTheme.typography.caption1Medium, - ) - } - } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) - Text( - text = stringResource(R.string.impression_step_description), - color = ReedTheme.colors.contentTertiary, - style = ReedTheme.typography.label1Medium, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing10)) - ReedRecordTextField( - recordState = state.impressionState, - recordHintRes = R.string.impression_step_hint, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .height(140.dp) - .onFocusChanged { focusState -> - isImpressionTextFieldFocused = focusState.isFocused - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default, - ), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) - Row( - modifier = Modifier - .fillMaxWidth() - .bringIntoViewRequester(bringIntoViewRequester), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - if (state.isImpressionGuideTooltipVisible) { - CustomTooltipBox( - messageResId = R.string.impression_guide_tooltip_message, - ) - } - - ReedButton( - onClick = { - state.eventSink(RecordRegisterUiEvent.OnImpressionGuideButtonClick) - }, - colorStyle = ReedButtonColorStyle.STROKE, - sizeStyle = smallRoundedButtonStyle, - text = stringResource(R.string.impression_step_guide), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(designR.drawable.ic_book_open), - contentDescription = "Impression Guide Icon", - ) - }, - ) - } - } - - ReedButton( - onClick = { - state.eventSink(RecordRegisterUiEvent.OnNextButtonClick) - }, - colorStyle = ReedButtonColorStyle.PRIMARY, - sizeStyle = largeButtonStyle, - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = ReedTheme.spacing.spacing5, - vertical = ReedTheme.spacing.spacing4, - ), - enabled = state.isNextButtonEnabled, - text = stringResource(R.string.record_finish_button_text), - multipleEventsCutterEnabled = true, - ) - } - - if (state.isImpressionGuideBottomSheetVisible) { - ImpressionGuideBottomSheet( - onDismissRequest = { - state.eventSink(RecordRegisterUiEvent.OnImpressionGuideBottomSheetDismiss) - }, - sheetState = impressionGuideBottomSheetState, - impressionState = state.impressionState, - impressionGuideList = state.impressionGuideList, - beforeSelectedImpressionGuide = state.beforeSelectedImpressionGuide, - selectedImpressionGuide = state.selectedImpressionGuide, - onGuideClick = { - state.eventSink(RecordRegisterUiEvent.OnSelectImpressionGuide(it)) - }, - onCloseButtonClick = { - coroutineScope.launch { - impressionGuideBottomSheetState.hide() - state.eventSink(RecordRegisterUiEvent.OnImpressionGuideBottomSheetDismiss) - } - }, - onSelectionConfirmButtonClick = { - coroutineScope.launch { - impressionGuideBottomSheetState.hide() - state.eventSink(RecordRegisterUiEvent.OnImpressionGuideConfirmed) - focusRequester.requestFocus() - } - }, - ) - } -} - -@ComponentPreview -@Composable -private fun ImpressionStepPreview() { - ReedTheme { - ImpressionStep( - state = RecordRegisterUiState( - eventSink = {}, - ), - ) - } -} diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt index 602ed8f78..5e790edd6 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt @@ -1,7 +1,6 @@ package com.ninecraft.booket.feature.record.step import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -10,9 +9,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.verticalScroll @@ -26,6 +27,7 @@ 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.focus.FocusDirection import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.vector.ImageVector @@ -39,13 +41,12 @@ import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle -import com.ninecraft.booket.core.designsystem.component.button.smallRoundedButtonStyle +import com.ninecraft.booket.core.designsystem.component.button.mediumButtonStyle import com.ninecraft.booket.core.designsystem.component.textfield.ReedRecordTextField import com.ninecraft.booket.core.designsystem.component.textfield.digitOnlyInputTransformation import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White import com.ninecraft.booket.feature.record.R -import com.ninecraft.booket.feature.record.component.CustomTooltipBox import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent import com.ninecraft.booket.feature.record.register.RecordRegisterUiState import com.skydoves.compose.stability.runtime.TraceRecomposition @@ -92,11 +93,69 @@ internal fun QuoteStep( ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing10)) Text( - text = stringResource(R.string.quote_step_page_label), + text = stringResource(R.string.quote_step_sentence_label), color = ReedTheme.colors.contentPrimary, style = ReedTheme.typography.body1Medium, ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ReedRecordTextField( + recordState = state.recordSentenceState, + recordHintRes = R.string.quote_step_sentence_hint, + modifier = Modifier + .fillMaxWidth() + .height(140.dp) + .onFocusChanged { focusState -> + isSentenceTextFieldFocused = focusState.isFocused + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Default, + ), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) + ReedButton( + onClick = { + state.eventSink(RecordRegisterUiEvent.OnSentenceScanButtonClick) + }, + colorStyle = ReedButtonColorStyle.TERTIARY, + sizeStyle = mediumButtonStyle, + text = stringResource(R.string.quote_step_scan_sentence), + modifier = Modifier + .fillMaxWidth() + .bringIntoViewRequester(bringIntoViewRequester), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_maximize), + contentDescription = "Scan Icon", + ) + }, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing12)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.quote_step_page_label), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + Text( + text = stringResource(R.string.select), + modifier = Modifier + .clip(RoundedCornerShape(ReedTheme.radius.xs)) + .background(color = ReedTheme.colors.bgSecondary) + .padding( + start = ReedTheme.spacing.spacing2, + top = ReedTheme.spacing.spacing05, + end = ReedTheme.spacing.spacing2, + bottom = ReedTheme.spacing.spacing05, + ), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.caption1Medium, + ) + } + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) ReedRecordTextField( recordState = state.recordPageState, recordHintRes = R.string.quote_step_page_hint, @@ -115,54 +174,43 @@ internal fun QuoteStep( .fillMaxWidth() .height(50.dp), ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) - Text( - text = stringResource(R.string.quote_step_sentence_label), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.body1Medium, - ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing12)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.quote_step_memo_label), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + Text( + text = stringResource(R.string.select), + modifier = Modifier + .clip(RoundedCornerShape(ReedTheme.radius.xs)) + .background(color = ReedTheme.colors.bgSecondary) + .padding( + start = ReedTheme.spacing.spacing2, + top = ReedTheme.spacing.spacing05, + end = ReedTheme.spacing.spacing2, + bottom = ReedTheme.spacing.spacing05, + ), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.caption1Medium, + ) + } Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) ReedRecordTextField( - recordState = state.recordSentenceState, - recordHintRes = R.string.quote_step_sentence_hint, + recordState = state.memoState, + recordHintRes = R.string.quote_step_memo_hint, modifier = Modifier .fillMaxWidth() - .height(140.dp) - .onFocusChanged { focusState -> - isSentenceTextFieldFocused = focusState.isFocused - }, + .height(140.dp), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Default, ), ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) - Row( - modifier = Modifier - .fillMaxWidth() - .bringIntoViewRequester(bringIntoViewRequester), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - if (state.isScanTooltipVisible) { - CustomTooltipBox(messageResId = R.string.scan_tooltip_message) - } - - ReedButton( - onClick = { - state.eventSink(RecordRegisterUiEvent.OnSentenceScanButtonClick) - }, - colorStyle = ReedButtonColorStyle.STROKE, - sizeStyle = smallRoundedButtonStyle, - text = stringResource(R.string.quote_step_scan_sentence), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(designR.drawable.ic_maximize), - contentDescription = "Scan Icon", - ) - }, - ) - } } ReedButton( diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionStepV2.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionStepV2.kt deleted file mode 100644 index 175b6b55e..000000000 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/EmotionStepV2.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.ninecraft.booket.feature.record.step_v2 - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.designsystem.ComponentPreview -import com.ninecraft.booket.core.designsystem.component.button.ReedButton -import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle -import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle -import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.core.designsystem.theme.White -import com.ninecraft.booket.core.model.Emotion -import com.ninecraft.booket.feature.record.R -import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent -import com.ninecraft.booket.feature.record.register.RecordRegisterUiState -import com.skydoves.compose.stability.runtime.TraceRecomposition -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class) -@TraceRecomposition -@Composable -internal fun EmotionStepV2( - state: RecordRegisterUiState, - modifier: Modifier = Modifier, -) { - val emotionDetailBottomSheetState = rememberModalBottomSheetState() - val coroutineScope = rememberCoroutineScope() - - Box( - modifier = modifier - .fillMaxSize() - .background(color = White), - ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = ReedTheme.spacing.spacing5) - .padding(bottom = 80.dp), - ) { - item { - Text( - text = stringResource(R.string.emotion_step_title), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.heading1Bold, - ) - } - item { - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) - } - item { - Text( - text = stringResource(R.string.emotion_step_description), - color = ReedTheme.colors.contentTertiary, - style = ReedTheme.typography.label1Medium, - ) - } - item { - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) - } - - items(state.emotions) { emotion -> - EmotionItem( - emotion = emotion, - selectedEmotionDetails = state.committedEmotionDetails[emotion] ?: persistentListOf(), - onClick = { - state.eventSink(RecordRegisterUiEvent.OnSelectEmotionV2(emotion)) - }, - isSelected = state.committedEmotion == emotion, - onEmotionDetailRemove = { detail -> - state.eventSink(RecordRegisterUiEvent.OnEmotionDetailRemoved(detail)) - }, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - } - } - - ReedButton( - onClick = { - state.eventSink(RecordRegisterUiEvent.OnNextButtonClick) - }, - colorStyle = ReedButtonColorStyle.PRIMARY, - sizeStyle = largeButtonStyle, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(horizontal = ReedTheme.spacing.spacing5) - .padding(bottom = ReedTheme.spacing.spacing4), - enabled = state.isNextButtonEnabled, - text = stringResource(R.string.record_finish_button_text), - multipleEventsCutterEnabled = false, - ) - } - - if (state.isEmotionDetailBottomSheetVisible) { - EmotionDetailBottomSheet( - emotion = state.selectedEmotion ?: Emotion.WARM, - emotionDetails = state.emotionDetails, - selectedEmotionDetail = state.selectedEmotionDetails[state.selectedEmotion] ?: persistentListOf(), - onDismissRequest = { - state.eventSink(RecordRegisterUiEvent.OnEmotionDetailBottomSheetDismiss) - }, - sheetState = emotionDetailBottomSheetState, - onCloseButtonClick = { - coroutineScope.launch { - emotionDetailBottomSheetState.hide() - state.eventSink(RecordRegisterUiEvent.OnEmotionDetailBottomSheetDismiss) - } - }, - onEmotionDetailToggled = { detail -> - state.eventSink(RecordRegisterUiEvent.OnEmotionDetailToggled(detail)) - }, - onSkipButtonClick = { - coroutineScope.launch { - emotionDetailBottomSheetState.hide() - state.eventSink(RecordRegisterUiEvent.OnEmotionDetailSkipped) - } - }, - onConfirmButtonClick = { - coroutineScope.launch { - emotionDetailBottomSheetState.hide() - state.eventSink(RecordRegisterUiEvent.OnEmotionDetailCommitted) - } - }, - ) - } -} - -@ComponentPreview -@Composable -private fun EmotionStepV2Preview() { - val emotions = Emotion.entries.toPersistentList() - - ReedTheme { - EmotionStepV2( - state = RecordRegisterUiState( - emotions = emotions, - eventSink = {}, - ), - ) - } -} diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/QuoteStepV2.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/QuoteStepV2.kt deleted file mode 100644 index 1c5b59f9a..000000000 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step_v2/QuoteStepV2.kt +++ /dev/null @@ -1,245 +0,0 @@ -package com.ninecraft.booket.feature.record.step_v2 - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.relocation.BringIntoViewRequester -import androidx.compose.foundation.relocation.bringIntoViewRequester -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.input.TextFieldLineLimits -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.designsystem.ComponentPreview -import com.ninecraft.booket.core.designsystem.component.button.ReedButton -import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle -import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle -import com.ninecraft.booket.core.designsystem.component.button.mediumButtonStyle -import com.ninecraft.booket.core.designsystem.component.textfield.ReedRecordTextField -import com.ninecraft.booket.core.designsystem.component.textfield.digitOnlyInputTransformation -import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.core.designsystem.theme.White -import com.ninecraft.booket.feature.record.R -import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent -import com.ninecraft.booket.feature.record.register.RecordRegisterUiState -import com.skydoves.compose.stability.runtime.TraceRecomposition -import kotlinx.coroutines.delay -import tech.thdev.compose.extensions.keyboard.state.foundation.rememberKeyboardVisible -import com.ninecraft.booket.core.designsystem.R as designR - -@TraceRecomposition -@Composable -internal fun QuoteStepV2( - state: RecordRegisterUiState, - modifier: Modifier = Modifier, -) { - val focusManager = LocalFocusManager.current - val scrollState = rememberScrollState() - val bringIntoViewRequester = remember { BringIntoViewRequester() } - val keyboardState by rememberKeyboardVisible() - var isSentenceTextFieldFocused by remember { mutableStateOf(false) } - - LaunchedEffect(keyboardState, isSentenceTextFieldFocused) { - if (keyboardState && isSentenceTextFieldFocused) { - delay(100) - bringIntoViewRequester.bringIntoView() - } - } - - Column( - modifier = modifier - .fillMaxSize() - .background(color = White) - .imePadding(), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - .padding(horizontal = ReedTheme.spacing.spacing5) - .verticalScroll(scrollState), - ) { - Text( - text = stringResource(R.string.quote_step_title), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.heading1Bold, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing10)) - Text( - text = stringResource(R.string.quote_step_sentence_label), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.body1Medium, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - ReedRecordTextField( - recordState = state.recordSentenceState, - recordHintRes = R.string.quote_step_sentence_hint, - modifier = Modifier - .fillMaxWidth() - .height(140.dp) - .onFocusChanged { focusState -> - isSentenceTextFieldFocused = focusState.isFocused - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default, - ), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) - ReedButton( - onClick = { - state.eventSink(RecordRegisterUiEvent.OnSentenceScanButtonClick) - }, - colorStyle = ReedButtonColorStyle.TERTIARY, - sizeStyle = mediumButtonStyle, - text = stringResource(R.string.quote_step_scan_sentence), - modifier = Modifier - .fillMaxWidth() - .bringIntoViewRequester(bringIntoViewRequester), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(designR.drawable.ic_maximize), - contentDescription = "Scan Icon", - ) - }, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing12)) - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.quote_step_page_label), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.body1Medium, - ) - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) - Text( - text = stringResource(R.string.select), - modifier = Modifier - .clip(RoundedCornerShape(ReedTheme.radius.xs)) - .background(color = ReedTheme.colors.bgSecondary) - .padding( - start = ReedTheme.spacing.spacing2, - top = ReedTheme.spacing.spacing05, - end = ReedTheme.spacing.spacing2, - bottom = ReedTheme.spacing.spacing05, - ), - color = ReedTheme.colors.contentTertiary, - style = ReedTheme.typography.caption1Medium, - ) - } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - ReedRecordTextField( - recordState = state.recordPageState, - recordHintRes = R.string.quote_step_page_hint, - inputTransformation = digitOnlyInputTransformation, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - lineLimits = TextFieldLineLimits.SingleLine, - isError = state.isPageError, - errorMessage = stringResource(R.string.quote_step_page_input_error), - onClear = { - state.eventSink(RecordRegisterUiEvent.OnClearClick) - }, - onNext = { - focusManager.moveFocus(FocusDirection.Down) - }, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing12)) - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.quote_step_memo_label), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.body1Medium, - ) - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) - Text( - text = stringResource(R.string.select), - modifier = Modifier - .clip(RoundedCornerShape(ReedTheme.radius.xs)) - .background(color = ReedTheme.colors.bgSecondary) - .padding( - start = ReedTheme.spacing.spacing2, - top = ReedTheme.spacing.spacing05, - end = ReedTheme.spacing.spacing2, - bottom = ReedTheme.spacing.spacing05, - ), - color = ReedTheme.colors.contentTertiary, - style = ReedTheme.typography.caption1Medium, - ) - } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - ReedRecordTextField( - recordState = state.memoState, - recordHintRes = R.string.quote_step_memo_hint, - modifier = Modifier - .fillMaxWidth() - .height(140.dp), - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default, - ), - ) - } - - ReedButton( - onClick = { - state.eventSink(RecordRegisterUiEvent.OnNextButtonClick) - }, - colorStyle = ReedButtonColorStyle.PRIMARY, - sizeStyle = largeButtonStyle, - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = ReedTheme.spacing.spacing5, - vertical = ReedTheme.spacing.spacing4, - ), - enabled = state.isNextButtonEnabled, - text = stringResource(R.string.record_next_button_text), - multipleEventsCutterEnabled = false, - ) - } -} - -@ComponentPreview -@Composable -private fun QuoteStepV2Preview() { - ReedTheme { - QuoteStepV2( - state = RecordRegisterUiState( - eventSink = {}, - ), - ) - } -} diff --git a/feature/record/src/main/res/values/strings.xml b/feature/record/src/main/res/values/strings.xml index 7f8837e42..3929d0856 100644 --- a/feature/record/src/main/res/values/strings.xml +++ b/feature/record/src/main/res/values/strings.xml @@ -2,8 +2,8 @@ 기록을 그만하고 나가시겠어요? 지금까지 기록한 내용은 저장되지 않습니다. - 확인 - 취소 + 확인 + 취소 수집할 문장이 화면에 모두 담기도록\n조정 후 하단 캡쳐 버튼을 눌러주세요 기록할 문장 선택 선택 완료 @@ -48,4 +48,6 @@ 더 자세한 감정을 선택 기록할 수 있어요. 건너뛰기 선택 완료 + 감정을 수정하시겠어요? + 기록된 감정이 삭제됩니다. diff --git a/feature/record/stability/record.stability b/feature/record/stability/record.stability index f0302675e..498b04db2 100644 --- a/feature/record/stability/record.stability +++ b/feature/record/stability/record.stability @@ -4,38 +4,6 @@ // Do not edit this file directly. To update it, run: // ./gradlew :record:stabilityDump -@Composable -internal fun com.ninecraft.booket.feature.record.component.CustomTooltipBox(messageResId: kotlin.Int): kotlin.Unit - skippable: true - restartable: true - params: - - messageResId: STABLE (primitive type) - -@Composable -public fun com.ninecraft.booket.feature.record.component.ImpressionGuideBottomSheet(onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, impressionState: androidx.compose.foundation.text.input.TextFieldState, impressionGuideList: kotlinx.collections.immutable.ImmutableList, beforeSelectedImpressionGuide: kotlin.String, selectedImpressionGuide: kotlin.String, onGuideClick: kotlin.Function1, onCloseButtonClick: kotlin.Function0, onSelectionConfirmButtonClick: kotlin.Function0): kotlin.Unit - skippable: true - restartable: true - params: - - onDismissRequest: STABLE (function type) - - sheetState: STABLE (marked @Stable or @Immutable) - - impressionState: STABLE (marked @Stable or @Immutable) - - impressionGuideList: STABLE (known stable type) - - beforeSelectedImpressionGuide: STABLE (String is immutable) - - selectedImpressionGuide: STABLE (String is immutable) - - onGuideClick: STABLE (function type) - - onCloseButtonClick: STABLE (function type) - - onSelectionConfirmButtonClick: STABLE (function type) - -@Composable -public fun com.ninecraft.booket.feature.record.component.ImpressionGuideBox(onClick: kotlin.Function0, impressionText: kotlin.String, modifier: androidx.compose.ui.Modifier, isSelected: kotlin.Boolean): kotlin.Unit - skippable: true - restartable: true - params: - - onClick: STABLE (function type) - - impressionText: STABLE (String is immutable) - - modifier: STABLE (marked @Stable or @Immutable) - - isSelected: STABLE (primitive type) - @Composable private fun com.ninecraft.booket.feature.record.ocr.CameraPreview(state: com.ninecraft.booket.feature.record.ocr.OcrUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true @@ -112,47 +80,12 @@ internal fun com.ninecraft.booket.feature.record.register.RecordRegisterUi(state - modifier: STABLE (marked @Stable or @Immutable) @Composable -private fun com.ninecraft.booket.feature.record.step.EmotionItem(emotion: com.ninecraft.booket.core.model.Emotion, onClick: kotlin.Function0, isSelected: kotlin.Boolean, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: true - restartable: true - params: - - emotion: STABLE (class with no mutable properties) - - onClick: STABLE (function type) - - isSelected: STABLE (primitive type) - - modifier: STABLE (marked @Stable or @Immutable) - -@Composable -public fun com.ninecraft.booket.feature.record.step.EmotionStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: true - restartable: true - params: - - state: STABLE (class with no mutable properties) - - modifier: STABLE (marked @Stable or @Immutable) - -@Composable -public fun com.ninecraft.booket.feature.record.step.ImpressionStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: true - restartable: true - params: - - state: STABLE (class with no mutable properties) - - modifier: STABLE (marked @Stable or @Immutable) - -@Composable -internal fun com.ninecraft.booket.feature.record.step.QuoteStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit +internal fun com.ninecraft.booket.feature.record.step.EmotionDetailBottomSheet(emotionGroup: com.ninecraft.booket.core.model.EmotionGroupModel, selectedEmotionDetailIds: kotlinx.collections.immutable.ImmutableList, onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, onCloseButtonClick: kotlin.Function0, onEmotionDetailToggled: kotlin.Function1, onSkipButtonClick: kotlin.Function0, onConfirmButtonClick: kotlin.Function0): kotlin.Unit skippable: true restartable: true params: - - state: STABLE (class with no mutable properties) - - modifier: STABLE (marked @Stable or @Immutable) - -@Composable -internal fun com.ninecraft.booket.feature.record.step_v2.EmotionDetailBottomSheet(emotion: com.ninecraft.booket.core.model.Emotion, emotionDetails: kotlinx.collections.immutable.ImmutableList, selectedEmotionDetail: kotlinx.collections.immutable.ImmutableList, onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, onCloseButtonClick: kotlin.Function0, onEmotionDetailToggled: kotlin.Function1, onSkipButtonClick: kotlin.Function0, onConfirmButtonClick: kotlin.Function0): kotlin.Unit - skippable: true - restartable: true - params: - - emotion: STABLE (class with no mutable properties) - - emotionDetails: STABLE (known stable type) - - selectedEmotionDetail: STABLE (known stable type) + - emotionGroup: STABLE (marked @Stable or @Immutable) + - selectedEmotionDetailIds: STABLE (known stable type) - onDismissRequest: STABLE (function type) - sheetState: STABLE (marked @Stable or @Immutable) - onCloseButtonClick: STABLE (function type) @@ -161,19 +94,19 @@ internal fun com.ninecraft.booket.feature.record.step_v2.EmotionDetailBottomShee - onConfirmButtonClick: STABLE (function type) @Composable -internal fun com.ninecraft.booket.feature.record.step_v2.EmotionItem(emotion: com.ninecraft.booket.core.model.Emotion, selectedEmotionDetails: kotlinx.collections.immutable.ImmutableList, onClick: kotlin.Function0, isSelected: kotlin.Boolean, onEmotionDetailRemove: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit +internal fun com.ninecraft.booket.feature.record.step.EmotionItem(emotionGroup: com.ninecraft.booket.core.model.EmotionGroupModel, selectedEmotionDetailIds: kotlinx.collections.immutable.ImmutableList, onClick: kotlin.Function0, isSelected: kotlin.Boolean, onEmotionDetailRemove: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: - - emotion: STABLE (class with no mutable properties) - - selectedEmotionDetails: STABLE (known stable type) + - emotionGroup: STABLE (marked @Stable or @Immutable) + - selectedEmotionDetailIds: STABLE (known stable type) - onClick: STABLE (function type) - isSelected: STABLE (primitive type) - onEmotionDetailRemove: STABLE (function type) - modifier: STABLE (marked @Stable or @Immutable) @Composable -internal fun com.ninecraft.booket.feature.record.step_v2.EmotionStepV2(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit +internal fun com.ninecraft.booket.feature.record.step.EmotionStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: @@ -181,7 +114,7 @@ internal fun com.ninecraft.booket.feature.record.step_v2.EmotionStepV2(state: co - modifier: STABLE (marked @Stable or @Immutable) @Composable -internal fun com.ninecraft.booket.feature.record.step_v2.QuoteStepV2(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit +internal fun com.ninecraft.booket.feature.record.step.QuoteStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt index 2d057b3b9..a70b02921 100644 --- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt +++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/Screens.kt @@ -1,5 +1,8 @@ package com.ninecraft.booket.feature.screens +import com.ninecraft.booket.core.model.EmotionCode +import com.ninecraft.booket.feature.screens.arguments.DetailEmotionArg +import com.ninecraft.booket.feature.screens.arguments.PrimaryEmotionArg import com.ninecraft.booket.feature.screens.arguments.RecordEditArgs import com.slack.circuit.runtime.screen.PopResult import com.slack.circuit.runtime.screen.Screen @@ -52,9 +55,15 @@ data class RecordDetailScreen(val recordId: String) : ReedScreen(name = ScreenNa data class RecordEditScreen(val recordInfo: RecordEditArgs) : ReedScreen(name = "RecordEdit()") @Parcelize -data class EmotionEditScreen(val emotion: String) : ReedScreen(name = "EmotionEdit()") { +data class EmotionEditScreen( + val primaryEmotionCode: EmotionCode, + val detailEmotionIds: List, +) : ReedScreen(name = "EmotionEdit()") { @Parcelize - data class Result(val emotion: String) : PopResult + data class Result( + val primaryEmotion: PrimaryEmotionArg, + val detailEmotions: List, + ) : PopResult } @Parcelize @@ -79,5 +88,5 @@ data object SplashScreen : ReedScreen(name = ScreenNames.SPLASH) data class RecordCardScreen( val quote: String, val bookTitle: String, - val emotion: String, + val emotionCode: EmotionCode, ) : ReedScreen(name = ScreenNames.RECORD_CARD) diff --git a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/arguments/RecordEditArgs.kt b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/arguments/RecordEditArgs.kt index d068dd9b6..cf18cad50 100644 --- a/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/arguments/RecordEditArgs.kt +++ b/feature/screens/src/main/kotlin/com/ninecraft/booket/feature/screens/arguments/RecordEditArgs.kt @@ -2,18 +2,34 @@ package com.ninecraft.booket.feature.screens.arguments import android.os.Parcelable import androidx.compose.runtime.Immutable +import com.ninecraft.booket.core.model.EmotionCode import kotlinx.parcelize.Parcelize @Immutable @Parcelize data class RecordEditArgs( val id: String, - val pageNumber: Int, + val pageNumber: Int?, val quote: String, val review: String, - val emotionTags: List, + val primaryEmotion: PrimaryEmotionArg, + val detailEmotions: List, val bookTitle: String, val bookPublisher: String, val bookCoverImageUrl: String, val author: String, ) : Parcelable + +@Immutable +@Parcelize +data class PrimaryEmotionArg( + val code: EmotionCode, + val displayName: String, +) : Parcelable + +@Immutable +@Parcelize +data class DetailEmotionArg( + val id: String, + val name: String, +) : Parcelable diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03a0fc802..665ead413 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ androidx-datastore = "1.2.0" androidx-camera = "1.5.2" ## Compose -androidx-compose-bom = "2025.12.01" +androidx-compose-bom = "2025.07.00" androidx-compose-material3 = "1.4.0" compose-stable-marker = "1.0.7" compose-effects = "0.1.4"