Skip to content

Commit 61974f9

Browse files
committed
[BOOK-188] feat: 도서 상세 화면 UI 구현 WIP
1 parent 1f3afae commit 61974f9

14 files changed

Lines changed: 560 additions & 12 deletions

File tree

core/common/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ dependencies {
1616
projects.core.model,
1717
projects.core.network,
1818

19+
libs.kotlinx.collections.immutable,
20+
1921
libs.logger,
2022
)
2123
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.ninecraft.booket.core.common.extensions
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.ui.graphics.Color
5+
import com.ninecraft.booket.core.model.Emotion
6+
7+
@Composable
8+
fun Emotion.toTextColor(): Color {
9+
return when (this) {
10+
Emotion.WARM -> Color(0xFFE3931B)
11+
Emotion.JOY -> Color(0xFFEE6B33)
12+
Emotion.TENSION -> Color(0xFF9A55E4)
13+
Emotion.SADNESS -> Color(0xFF2872E9)
14+
}
15+
}
16+
17+
@Composable
18+
fun Emotion.toBackgroundColor(): Color {
19+
return when (this) {
20+
Emotion.WARM -> Color(0xFFFFF5D3)
21+
Emotion.JOY -> Color(0xFFFFEBE3)
22+
Emotion.TENSION -> Color(0xFFF3E8FF)
23+
Emotion.SADNESS -> Color(0xFFE1ECFF)
24+
}
25+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.ninecraft.booket.core.common.util
2+
3+
import com.ninecraft.booket.core.model.EmotionModel
4+
5+
data class EmotionAnalysisResult(
6+
val topEmotions: List<EmotionModel>,
7+
val displayType: EmotionDisplayType,
8+
)
9+
10+
enum class EmotionDisplayType {
11+
SINGLE, // 1개 감정이 1위
12+
DUAL, // 2개 감정이 공동 1위
13+
BALANCED, // 3개 이상 감정이 공동 1위
14+
}
15+
16+
fun analyzeEmotions(emotions: List<EmotionModel>): EmotionAnalysisResult {
17+
if (emotions.isEmpty()) {
18+
return EmotionAnalysisResult(emptyList(), EmotionDisplayType.BALANCED)
19+
}
20+
21+
val maxCount = emotions.maxOf { it.count }
22+
val topEmotions = emotions.filter { it.count == maxCount }
23+
24+
val displayType = when (topEmotions.size) {
25+
1 -> EmotionDisplayType.SINGLE
26+
2 -> EmotionDisplayType.DUAL
27+
else -> EmotionDisplayType.BALANCED
28+
}
29+
30+
return EmotionAnalysisResult(topEmotions, displayType)
31+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.ninecraft.booket.core.common.util
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.remember
5+
import androidx.compose.ui.graphics.Color
6+
import androidx.compose.ui.text.AnnotatedString
7+
import androidx.compose.ui.text.SpanStyle
8+
import androidx.compose.ui.text.TextStyle
9+
import androidx.compose.ui.text.buildAnnotatedString
10+
import androidx.compose.ui.text.withStyle
11+
import com.ninecraft.booket.core.model.EmotionModel
12+
import kotlinx.collections.immutable.ImmutableList
13+
14+
@Composable
15+
fun buildEmotionText(
16+
emotions: ImmutableList<EmotionModel>,
17+
brandColor: Color,
18+
secondaryColor: Color,
19+
emotionTextStyle: TextStyle,
20+
regularTextStyle: TextStyle,
21+
): AnnotatedString {
22+
val analysisResult = remember(emotions) { analyzeEmotions(emotions) }
23+
24+
return when (analysisResult.displayType) {
25+
EmotionDisplayType.SINGLE -> {
26+
val emotion = analysisResult.topEmotions.first()
27+
buildAnnotatedString {
28+
withStyle(SpanStyle(color = secondaryColor, fontSize = regularTextStyle.fontSize, fontWeight = regularTextStyle.fontWeight)) {
29+
append("이 책에서 ")
30+
}
31+
withStyle(SpanStyle(color = brandColor, fontSize = emotionTextStyle.fontSize, fontWeight = emotionTextStyle.fontWeight)) {
32+
append(emotion.type.displayName)
33+
}
34+
withStyle(SpanStyle(color = secondaryColor, fontSize = regularTextStyle.fontSize, fontWeight = regularTextStyle.fontWeight)) {
35+
append(" 감정을 많이 느꼈어요")
36+
}
37+
}
38+
}
39+
EmotionDisplayType.DUAL -> {
40+
val emotions = analysisResult.topEmotions
41+
buildAnnotatedString {
42+
withStyle(SpanStyle(color = secondaryColor, fontSize = regularTextStyle.fontSize, fontWeight = regularTextStyle.fontWeight)) {
43+
append("이 책에서 ")
44+
}
45+
emotions.forEachIndexed { index, emotion ->
46+
if (index > 0) {
47+
withStyle(SpanStyle(color = secondaryColor, fontSize = regularTextStyle.fontSize, fontWeight = regularTextStyle.fontWeight)) {
48+
append(", ")
49+
}
50+
}
51+
withStyle(SpanStyle(color = brandColor, fontSize = emotionTextStyle.fontSize, fontWeight = emotionTextStyle.fontWeight)) {
52+
append(emotion.type.displayName)
53+
}
54+
}
55+
withStyle(SpanStyle(color = secondaryColor, fontSize = regularTextStyle.fontSize, fontWeight = regularTextStyle.fontWeight)) {
56+
append(" 감정을 많이 느꼈어요")
57+
}
58+
}
59+
}
60+
EmotionDisplayType.BALANCED -> {
61+
buildAnnotatedString {
62+
withStyle(SpanStyle(color = secondaryColor, fontSize = regularTextStyle.fontSize, fontWeight = regularTextStyle.fontWeight)) {
63+
append("이 책에서 여러 감정이 고르게 담겼어요")
64+
}
65+
}
66+
}
67+
}
68+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.ninecraft.booket.core.model
2+
3+
import androidx.compose.runtime.Stable
4+
5+
@Stable
6+
data class EmotionModel(
7+
val type: Emotion,
8+
val count: Int,
9+
)
10+
11+
enum class Emotion(
12+
val displayName: String,
13+
) {
14+
WARM("따뜻함"),
15+
JOY("즐거움"),
16+
TENSION("긴장감"),
17+
SADNESS("슬픔"),
18+
}

feature/detail/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ ksp {
1616

1717
dependencies {
1818
implementations(
19+
libs.kotlinx.collections.immutable,
20+
1921
libs.logger,
2022
)
2123
}

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package com.ninecraft.booket.feature.detail.book
22

3-
import android.R.attr.onClick
4-
import android.R.attr.text
53
import androidx.compose.foundation.layout.Column
64
import androidx.compose.foundation.layout.Row
75
import androidx.compose.foundation.layout.Spacer
8-
import androidx.compose.foundation.layout.fillMaxSize
96
import androidx.compose.foundation.layout.fillMaxWidth
107
import androidx.compose.foundation.layout.height
118
import androidx.compose.foundation.layout.padding
@@ -25,7 +22,6 @@ import androidx.compose.ui.res.vectorResource
2522
import androidx.compose.ui.text.style.TextOverflow
2623
import androidx.compose.ui.unit.dp
2724
import com.ninecraft.booket.core.designsystem.ComponentPreview
28-
import com.ninecraft.booket.core.designsystem.R as designR
2925
import com.ninecraft.booket.core.designsystem.component.NetworkImage
3026
import com.ninecraft.booket.core.designsystem.component.ReedDivider
3127
import com.ninecraft.booket.core.designsystem.component.button.ReedButton
@@ -35,9 +31,11 @@ import com.ninecraft.booket.core.designsystem.theme.ReedTheme
3531
import com.ninecraft.booket.core.ui.component.ReedBackTopAppBar
3632
import com.ninecraft.booket.core.ui.component.ReedFullScreen
3733
import com.ninecraft.booket.feature.detail.book.component.CollectedSeed
34+
import com.ninecraft.booket.feature.detail.book.component.RecordsCollection
3835
import com.ninecraft.booket.feature.screens.BookDetailScreen
3936
import com.slack.circuit.codegen.annotations.CircuitInject
4037
import dagger.hilt.android.components.ActivityRetainedComponent
38+
import com.ninecraft.booket.core.designsystem.R as designR
4139

4240
@CircuitInject(BookDetailScreen::class, ActivityRetainedComponent::class)
4341
@Composable
@@ -160,8 +158,8 @@ internal fun BookDetailContent(
160158
)
161159
}
162160
CollectedSeed(state = state)
163-
Spacer(Modifier.height(ReedTheme.spacing.spacing6))
164161
ReedDivider()
162+
RecordsCollection(state = state)
165163
}
166164
}
167165

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,61 @@
11
package com.ninecraft.booket.feature.detail.book
22

33
import com.ninecraft.booket.core.common.R
4-
import com.ninecraft.booket.core.common.constants.BookStatus
4+
import com.ninecraft.booket.core.model.Emotion
5+
import com.ninecraft.booket.core.model.EmotionModel
6+
import com.ninecraft.booket.core.model.RecordRegisterModel
57
import com.slack.circuit.runtime.CircuitUiEvent
68
import com.slack.circuit.runtime.CircuitUiState
9+
import kotlinx.collections.immutable.ImmutableList
10+
import kotlinx.collections.immutable.persistentListOf
711
import java.util.UUID
812

913
data class BookDetailUiState(
14+
val emotionList: ImmutableList<EmotionModel> = persistentListOf(
15+
EmotionModel(
16+
type = Emotion.WARM,
17+
count = 3,
18+
),
19+
EmotionModel(
20+
type = Emotion.JOY,
21+
count = 1,
22+
),
23+
EmotionModel(
24+
type = Emotion.TENSION,
25+
count = 2,
26+
),
27+
EmotionModel(
28+
type = Emotion.SADNESS,
29+
count = 2,
30+
),
31+
),
32+
val currentRecordSort: RecordSort = RecordSort.PAGE_ASCENDING,
33+
val recordCollections: ImmutableList<RecordRegisterModel> = persistentListOf(
34+
RecordRegisterModel(
35+
id = "0",
36+
pageNumber = 12,
37+
quote = "“책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다.책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다.“",
38+
createdAt = "2025.06.25",
39+
),
40+
RecordRegisterModel(
41+
id = "1",
42+
pageNumber = 13,
43+
quote = "“책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다.책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다.“",
44+
createdAt = "2025.06.25",
45+
),
46+
RecordRegisterModel(
47+
id = "2",
48+
pageNumber = 14,
49+
quote = "“책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다.책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다.“",
50+
createdAt = "2025.06.25",
51+
),
52+
RecordRegisterModel(
53+
id = "3",
54+
pageNumber = 15,
55+
quote = "“책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다.책을 읽으면 차분해지며 숲으로 둘러싸인 여름 별장 속으로 간 것 같은 기분이 든다. 그 곳에서 그들이 품은 건축에 대한 이상과 삶을 구경하는 것만으로도 충분했다.“",
56+
createdAt = "2025.06.25",
57+
),
58+
),
1059
val sideEffect: BookDetailSideEffect? = null,
1160
val eventSink: (BookDetailUiEvent) -> Unit,
1261
) : CircuitUiState
Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,100 @@
11
package com.ninecraft.booket.feature.detail.book.component
22

3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.border
5+
import androidx.compose.foundation.layout.Arrangement
36
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.height
12+
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.foundation.shape.RoundedCornerShape
14+
import androidx.compose.material3.Text
415
import androidx.compose.runtime.Composable
16+
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.draw.clip
18+
import androidx.compose.ui.res.stringResource
19+
import androidx.compose.ui.text.style.TextAlign
20+
import androidx.compose.ui.unit.dp
21+
import com.ninecraft.booket.core.common.util.buildEmotionText
22+
import com.ninecraft.booket.core.designsystem.ComponentPreview
23+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
24+
import com.ninecraft.booket.feature.detail.R
525
import com.ninecraft.booket.feature.detail.book.BookDetailUiState
626

27+
// TODO 필요한 파라미터만 선언하여 사용하기
728
@Composable
829
internal fun CollectedSeed(
9-
state: BookDetailUiState
30+
state: BookDetailUiState,
1031
) {
11-
Box {}
32+
Column(
33+
modifier = Modifier
34+
.fillMaxWidth()
35+
.padding(
36+
start = ReedTheme.spacing.spacing5,
37+
top = ReedTheme.spacing.spacing5,
38+
end = ReedTheme.spacing.spacing5,
39+
bottom = ReedTheme.spacing.spacing6,
40+
)
41+
.clip(RoundedCornerShape(ReedTheme.radius.md))
42+
.background(ReedTheme.colors.baseSecondary),
43+
) {
44+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4))
45+
Text(
46+
text = stringResource(R.string.collected_seed_title),
47+
modifier = Modifier.padding(horizontal = ReedTheme.spacing.spacing4),
48+
color = ReedTheme.colors.contentSecondary,
49+
style = ReedTheme.typography.body2Medium,
50+
)
51+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5))
52+
Row(
53+
modifier = Modifier.fillMaxWidth(),
54+
horizontalArrangement = Arrangement.SpaceEvenly,
55+
) {
56+
state.emotionList.forEach { emotion ->
57+
SeedItem(emotion = emotion)
58+
}
59+
}
60+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5))
61+
Box(
62+
modifier = Modifier
63+
.fillMaxWidth()
64+
.padding(horizontal = ReedTheme.spacing.spacing4)
65+
.clip(RoundedCornerShape(ReedTheme.radius.sm))
66+
.background(ReedTheme.colors.basePrimary)
67+
.padding(ReedTheme.spacing.spacing3)
68+
.border(
69+
width = 1.dp,
70+
color = ReedTheme.colors.basePrimary,
71+
shape = RoundedCornerShape(ReedTheme.radius.sm),
72+
),
73+
) {
74+
Text(
75+
text = buildEmotionText(
76+
emotions = state.emotionList,
77+
brandColor = ReedTheme.colors.contentBrand,
78+
secondaryColor = ReedTheme.colors.contentSecondary,
79+
emotionTextStyle = ReedTheme.typography.label2SemiBold,
80+
regularTextStyle = ReedTheme.typography.label2Regular,
81+
),
82+
modifier = Modifier.fillMaxWidth(),
83+
textAlign = TextAlign.Center,
84+
)
85+
}
86+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4))
87+
}
88+
}
89+
90+
@ComponentPreview
91+
@Composable
92+
private fun CollectedSeedPreview() {
93+
ReedTheme {
94+
CollectedSeed(
95+
state = BookDetailUiState(
96+
eventSink = {},
97+
),
98+
)
99+
}
12100
}

0 commit comments

Comments
 (0)