Skip to content

Commit 1d183ca

Browse files
authored
Merge pull request #73 from YAPP-Github/BOOK-165-feature/#67
feat: 독서 기록 플로우 화면 - 감상평, 감정선택 UI 구현
2 parents 1c4196c + 56d9cfa commit 1d183ca

File tree

12 files changed

+706
-134
lines changed

12 files changed

+706
-134
lines changed

core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/RecordStep.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.ninecraft.booket.core.designsystem
22

33
enum class RecordStep {
4-
REGISTER,
5-
APPRECIATION,
4+
QUOTE,
65
EMOTION,
6+
IMPRESSION,
77
;
88

99
val value: Int get() = ordinal

core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/RecordProgressBar.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ fun RecordProgressBar(
3030
.height(6.dp)
3131
.clip(RoundedCornerShape(ReedTheme.radius.full))
3232
.background(
33-
color = if (currentStep.value == index) {
33+
color = if (index <= currentStep.ordinal) {
3434
ReedTheme.colors.bgPrimary
3535
} else {
3636
ReedTheme.colors.bgDisabled

core/network/src/main/kotlin/com/ninecraft/booket/core/network/service/ReedService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,6 @@ interface ReedService {
5959
@Query("status") status: String? = null,
6060
@Query("page") page: Int,
6161
@Query("size") size: Int,
62-
@Query("sort") sort: String = "date_desc",
62+
@Query("sort") sort: String = "CREATED_DATE_DESC",
6363
): LibraryResponse
6464
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.ninecraft.booket.feature.record.component
2+
3+
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Row
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material3.ExperimentalMaterial3Api
11+
import androidx.compose.material3.Icon
12+
import androidx.compose.material3.SheetState
13+
import androidx.compose.material3.SheetValue
14+
import androidx.compose.material3.Text
15+
import androidx.compose.runtime.Composable
16+
import androidx.compose.ui.Alignment
17+
import androidx.compose.ui.Modifier
18+
import androidx.compose.ui.graphics.vector.ImageVector
19+
import androidx.compose.ui.res.stringResource
20+
import androidx.compose.ui.res.vectorResource
21+
import androidx.compose.ui.text.style.TextAlign
22+
import com.ninecraft.booket.core.common.extensions.clickableSingle
23+
import com.ninecraft.booket.core.designsystem.ComponentPreview
24+
import com.ninecraft.booket.core.designsystem.component.button.ReedButton
25+
import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle
26+
import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle
27+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
28+
import com.ninecraft.booket.core.ui.component.ReedBottomSheet
29+
import com.ninecraft.booket.feature.record.R
30+
import kotlinx.collections.immutable.ImmutableList
31+
import kotlinx.collections.immutable.toPersistentList
32+
import com.ninecraft.booket.core.designsystem.R as designR
33+
34+
@OptIn(ExperimentalMaterial3Api::class)
35+
@Composable
36+
fun ImpressionGuideBottomSheet(
37+
onDismissRequest: () -> Unit,
38+
sheetState: SheetState,
39+
impressionGuideList: ImmutableList<String>,
40+
selectedImpressionGuide: String,
41+
onGuideClick: (Int) -> Unit,
42+
onCloseButtonClick: () -> Unit,
43+
onSelectionConfirmButtonClick: () -> Unit,
44+
) {
45+
ReedBottomSheet(
46+
onDismissRequest = {
47+
onDismissRequest()
48+
},
49+
sheetState = sheetState,
50+
) {
51+
Column(
52+
modifier = Modifier
53+
.padding(
54+
start = ReedTheme.spacing.spacing5,
55+
top = ReedTheme.spacing.spacing5,
56+
end = ReedTheme.spacing.spacing5,
57+
),
58+
horizontalAlignment = Alignment.CenterHorizontally,
59+
) {
60+
Row(
61+
modifier = Modifier.fillMaxWidth(),
62+
horizontalArrangement = Arrangement.SpaceBetween,
63+
) {
64+
Text(
65+
text = stringResource(R.string.impression_guide_bottomsheet_title),
66+
color = ReedTheme.colors.contentPrimary,
67+
textAlign = TextAlign.Center,
68+
style = ReedTheme.typography.heading2SemiBold,
69+
)
70+
Icon(
71+
imageVector = ImageVector.vectorResource(designR.drawable.ic_close),
72+
contentDescription = "Close Icon",
73+
modifier = Modifier.clickableSingle {
74+
onCloseButtonClick()
75+
},
76+
)
77+
}
78+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2))
79+
Text(
80+
text = stringResource(R.string.impression_guide_bottomsheet_description),
81+
modifier = Modifier.fillMaxWidth(),
82+
color = ReedTheme.colors.contentPrimary,
83+
style = ReedTheme.typography.label1Medium,
84+
)
85+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5))
86+
Column(
87+
modifier = Modifier.fillMaxWidth(),
88+
verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2),
89+
) {
90+
impressionGuideList.forEachIndexed { index, guide ->
91+
ImpressionGuideBox(
92+
onClick = {
93+
onGuideClick(index)
94+
},
95+
impressionText = guide,
96+
isSelected = selectedImpressionGuide == guide,
97+
)
98+
}
99+
}
100+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3))
101+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4))
102+
ReedButton(
103+
onClick = {
104+
onSelectionConfirmButtonClick()
105+
},
106+
sizeStyle = largeButtonStyle,
107+
colorStyle = ReedButtonColorStyle.PRIMARY,
108+
modifier = Modifier.fillMaxWidth(),
109+
enabled = selectedImpressionGuide.isNotEmpty(),
110+
text = stringResource(R.string.impression_guide_bottomsheet_selection_confirm),
111+
)
112+
Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4))
113+
}
114+
}
115+
}
116+
117+
@OptIn(ExperimentalMaterial3Api::class)
118+
@ComponentPreview
119+
@Composable
120+
private fun ImpressionGuideBottomSheetPreview() {
121+
val sheetState = SheetState(
122+
skipPartiallyExpanded = true,
123+
initialValue = SheetValue.Expanded,
124+
positionalThreshold = { 0f },
125+
velocityThreshold = { 0f },
126+
)
127+
val impressionGuideList = listOf(
128+
"에서 위로 받았다",
129+
"이 마음에 남았다",
130+
"에서 작가의 의도가 궁금하다",
131+
"에 대한 다른 사람들의 생각이 궁금하다",
132+
"에서 크게 공감이 된다",
133+
"을 보고 예전 기억이 났다",
134+
"에서 문장에 머물렀다",
135+
).toPersistentList()
136+
137+
ReedTheme {
138+
ImpressionGuideBottomSheet(
139+
onDismissRequest = {},
140+
sheetState = sheetState,
141+
impressionGuideList = impressionGuideList,
142+
selectedImpressionGuide = "",
143+
onGuideClick = {},
144+
onCloseButtonClick = {},
145+
onSelectionConfirmButtonClick = {},
146+
)
147+
}
148+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.ninecraft.booket.feature.record.component
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.border
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.foundation.shape.RoundedCornerShape
10+
import androidx.compose.material3.Text
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Alignment
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.draw.clip
15+
import androidx.compose.ui.graphics.Color
16+
import androidx.compose.ui.res.stringResource
17+
import androidx.compose.ui.unit.dp
18+
import com.ninecraft.booket.core.common.extensions.clickableSingle
19+
import com.ninecraft.booket.core.designsystem.ComponentPreview
20+
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
21+
import com.ninecraft.booket.core.designsystem.theme.White
22+
import com.ninecraft.booket.feature.record.R
23+
24+
@Composable
25+
fun ImpressionGuideBox(
26+
onClick: () -> Unit,
27+
impressionText: String,
28+
modifier: Modifier = Modifier,
29+
isSelected: Boolean = false,
30+
) {
31+
val bgColor = if (isSelected) ReedTheme.colors.bgTertiary else White
32+
val borderColor = if (isSelected) ReedTheme.colors.borderBrand else ReedTheme.colors.borderPrimary
33+
34+
Box(
35+
modifier = modifier
36+
.fillMaxWidth()
37+
.background(
38+
color = bgColor,
39+
shape = RoundedCornerShape(ReedTheme.radius.sm),
40+
)
41+
.border(
42+
width = 1.dp,
43+
color = borderColor,
44+
shape = RoundedCornerShape(ReedTheme.radius.sm),
45+
)
46+
.clip(RoundedCornerShape(ReedTheme.radius.sm))
47+
.clickableSingle {
48+
onClick()
49+
}
50+
.padding(
51+
horizontal = ReedTheme.spacing.spacing4,
52+
vertical = ReedTheme.spacing.spacing4,
53+
),
54+
) {
55+
Row(verticalAlignment = Alignment.Bottom) {
56+
Text(
57+
text = stringResource(R.string.impression_guide_blank),
58+
color = Color(0xFFD6D6D6),
59+
style = ReedTheme.typography.label1SemiBold,
60+
)
61+
Text(
62+
text = impressionText,
63+
color = ReedTheme.colors.contentPrimary,
64+
style = ReedTheme.typography.label1SemiBold,
65+
)
66+
}
67+
}
68+
}
69+
70+
@ComponentPreview
71+
@Composable
72+
private fun ImpressionGuideBoxPreview() {
73+
ReedTheme {
74+
ImpressionGuideBox(
75+
onClick = {},
76+
impressionText = "에서 위로 받았다",
77+
)
78+
}
79+
}

feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
66
import androidx.compose.runtime.getValue
77
import androidx.compose.runtime.mutableStateOf
88
import androidx.compose.runtime.setValue
9+
import com.ninecraft.booket.core.designsystem.RecordStep
910
import com.ninecraft.booket.feature.screens.RecordScreen
1011
import com.slack.circuit.codegen.annotations.CircuitInject
1112
import com.slack.circuit.retained.rememberRetained
@@ -15,21 +16,51 @@ import dagger.assisted.Assisted
1516
import dagger.assisted.AssistedFactory
1617
import dagger.assisted.AssistedInject
1718
import dagger.hilt.android.components.ActivityRetainedComponent
19+
import kotlinx.collections.immutable.toPersistentList
1820

1921
class RecordRegisterPresenter @AssistedInject constructor(
2022
@Assisted private val navigator: Navigator,
21-
) : Presenter<RecordUiState> {
23+
) : Presenter<RecordRegisterUiState> {
2224

2325
@Composable
24-
override fun present(): RecordUiState {
26+
override fun present(): RecordRegisterUiState {
27+
var currentStep by rememberRetained { mutableStateOf(RecordStep.QUOTE) }
2528
val recordPageState = rememberTextFieldState()
2629
val recordSentenceState = rememberTextFieldState()
30+
val impressionGuideList by rememberRetained {
31+
mutableStateOf(
32+
listOf(
33+
"에서 위로 받았다",
34+
"이 마음에 남았다",
35+
"에서 작가의 의도가 궁금하다",
36+
"에 대한 다른 사람들의 생각이 궁금하다",
37+
"에서 크게 공감이 된다",
38+
"을 보고 예전 기억이 났다",
39+
"에서 문장에 머물렀다",
40+
).toPersistentList(),
41+
)
42+
}
43+
var selectedImpressionGuide by rememberRetained { mutableStateOf("") }
44+
val impressionState = rememberTextFieldState()
45+
var isImpressionGuideBottomSheetVisible by rememberRetained { mutableStateOf(false) }
2746
var isExitDialogVisible by rememberRetained { mutableStateOf(false) }
2847

2948
fun handleEvent(event: RecordRegisterUiEvent) {
3049
when (event) {
3150
is RecordRegisterUiEvent.OnBackButtonClick -> {
32-
isExitDialogVisible = true
51+
when (currentStep) {
52+
RecordStep.QUOTE -> {
53+
isExitDialogVisible = true
54+
}
55+
56+
RecordStep.EMOTION -> {
57+
currentStep = RecordStep.QUOTE
58+
}
59+
60+
RecordStep.IMPRESSION -> {
61+
currentStep = RecordStep.EMOTION
62+
}
63+
}
3364
}
3465

3566
is RecordRegisterUiEvent.OnClearClick -> {
@@ -45,14 +76,61 @@ class RecordRegisterPresenter @AssistedInject constructor(
4576
}
4677

4778
is RecordRegisterUiEvent.OnSentenceScanButtonClick -> {}
48-
is RecordRegisterUiEvent.OnNextButtonClick -> {}
79+
80+
is RecordRegisterUiEvent.OnSelectEmotion -> {}
81+
82+
is RecordRegisterUiEvent.OnImpressionGuideButtonClick -> {
83+
selectedImpressionGuide = ""
84+
impressionState.edit {
85+
replace(0, length, "")
86+
}
87+
isImpressionGuideBottomSheetVisible = true
88+
}
89+
90+
is RecordRegisterUiEvent.OnSelectImpressionGuide -> {
91+
val index = event.index
92+
if (index in impressionGuideList.indices) {
93+
selectedImpressionGuide = impressionGuideList[index]
94+
impressionState.edit {
95+
replace(0, length, "")
96+
append(selectedImpressionGuide)
97+
}
98+
}
99+
}
100+
101+
is RecordRegisterUiEvent.OnSelectionConfirmed -> {}
102+
103+
is RecordRegisterUiEvent.OnImpressionGuideBottomSheetDismiss -> {
104+
isImpressionGuideBottomSheetVisible = false
105+
}
106+
107+
is RecordRegisterUiEvent.OnNextButtonClick -> {
108+
when (currentStep) {
109+
RecordStep.QUOTE -> {
110+
currentStep = RecordStep.EMOTION
111+
}
112+
113+
RecordStep.EMOTION -> {
114+
currentStep = RecordStep.IMPRESSION
115+
}
116+
117+
RecordStep.IMPRESSION -> {
118+
// TODO: (기록 완료 API 성공 시) 기록 상세 화면 이동
119+
}
120+
}
121+
}
49122
}
50123
}
51124

52-
return RecordUiState(
125+
return RecordRegisterUiState(
126+
currentStep = currentStep,
53127
recordPageState = recordPageState,
54128
recordSentenceState = recordSentenceState,
129+
impressionState = impressionState,
130+
impressionGuideList = impressionGuideList,
131+
isImpressionGuideBottomSheetVisible = isImpressionGuideBottomSheetVisible,
55132
isExitDialogVisible = isExitDialogVisible,
133+
selectedImpressionGuide = selectedImpressionGuide,
56134
eventSink = ::handleEvent,
57135
)
58136
}

0 commit comments

Comments
 (0)