Skip to content

Commit df35b81

Browse files
committed
FEAT: 감정구슬 화면 UI 구현
1 parent 52a7008 commit df35b81

File tree

6 files changed

+281
-0
lines changed

6 files changed

+281
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package com.threegap.bitnagil.presentation.emotion
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.clickable
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.fillMaxSize
11+
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.layout.height
13+
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.layout.size
15+
import androidx.compose.foundation.layout.widthIn
16+
import androidx.compose.foundation.lazy.grid.GridCells
17+
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
18+
import androidx.compose.foundation.lazy.grid.items
19+
import androidx.compose.material3.Text
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.runtime.collectAsState
22+
import androidx.compose.runtime.getValue
23+
import androidx.compose.ui.Alignment
24+
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.res.painterResource
26+
import androidx.compose.ui.text.style.TextAlign
27+
import androidx.compose.ui.tooling.preview.Preview
28+
import androidx.compose.ui.unit.dp
29+
import androidx.hilt.navigation.compose.hiltViewModel
30+
import com.threegap.bitnagil.designsystem.BitnagilTheme
31+
import com.threegap.bitnagil.presentation.common.flow.collectAsEffect
32+
import com.threegap.bitnagil.presentation.emotion.model.Emotion
33+
import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionSideEffect
34+
import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionState
35+
36+
@Composable
37+
fun EmotionScreenContainer(
38+
viewModel: EmotionViewModel = hiltViewModel(),
39+
navigateToBack: () -> Unit,
40+
) {
41+
val state by viewModel.stateFlow.collectAsState()
42+
43+
viewModel.sideEffectFlow.collectAsEffect { sideEffect ->
44+
when (sideEffect) {
45+
EmotionSideEffect.NavigateToBack -> navigateToBack()
46+
}
47+
}
48+
49+
EmotionScreen(
50+
state = state,
51+
onClickPreviousButton = navigateToBack,
52+
onClickEmotion = viewModel::selectEmotion,
53+
)
54+
}
55+
56+
@Composable
57+
private fun EmotionScreen(
58+
state: EmotionState,
59+
onClickPreviousButton: () -> Unit,
60+
onClickEmotion: (Emotion) -> Unit,
61+
) {
62+
Column(
63+
modifier = Modifier
64+
.fillMaxSize()
65+
.background(color = BitnagilTheme.colors.white),
66+
horizontalAlignment = Alignment.CenterHorizontally
67+
) {
68+
Box(
69+
modifier = Modifier
70+
.height(54.dp)
71+
.fillMaxWidth(),
72+
) {
73+
Box(
74+
modifier = Modifier
75+
.padding(start = 2.dp)
76+
.size(48.dp)
77+
.background(BitnagilTheme.colors.black)
78+
.align(Alignment.CenterStart)
79+
.clickable(onClick = onClickPreviousButton),
80+
)
81+
}
82+
83+
Spacer(modifier = Modifier.height(32.dp))
84+
85+
Text("오늘의 감정구슬을 골라보세요", style = BitnagilTheme.typography.title2Bold.copy(color = BitnagilTheme.colors.navy500), textAlign = TextAlign.Center)
86+
87+
Spacer(modifier = Modifier.height(6.dp))
88+
89+
Text("감정구슬을 등록하면 루틴을 추천받아요!", style = BitnagilTheme.typography.subtitle1Regular.copy(color = BitnagilTheme.colors.navy300), textAlign = TextAlign.Center)
90+
91+
Spacer(modifier = Modifier.height(64.dp))
92+
93+
LazyVerticalGrid(
94+
columns = GridCells.Fixed(3),
95+
modifier = Modifier.padding(horizontal = 40.dp).widthIn(300.dp),
96+
horizontalArrangement = Arrangement.spacedBy(32.dp),
97+
verticalArrangement = Arrangement.spacedBy(32.dp)
98+
) {
99+
items(state.emotions) { emotion ->
100+
Column(
101+
horizontalAlignment = Alignment.CenterHorizontally
102+
) {
103+
Image(
104+
painter = painterResource(id = emotion.imageResourceId),
105+
contentDescription = null,
106+
modifier = Modifier.size(72.dp).clickable {
107+
onClickEmotion(emotion)
108+
}
109+
)
110+
Spacer(modifier = Modifier.height(12.dp))
111+
Text(emotion.emotionName, style = BitnagilTheme.typography.body1Regular.copy(color = BitnagilTheme.colors.coolGray20))
112+
}
113+
}
114+
}
115+
}
116+
}
117+
118+
@Preview
119+
@Composable
120+
fun MyPageScreenPreview() {
121+
BitnagilTheme {
122+
EmotionScreen(
123+
state = EmotionState(
124+
emotions = Emotion.entries,
125+
isLoading = false,
126+
),
127+
onClickEmotion = { _ -> },
128+
onClickPreviousButton = {}
129+
)
130+
}
131+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.threegap.bitnagil.presentation.emotion
2+
3+
import androidx.lifecycle.SavedStateHandle
4+
import androidx.lifecycle.viewModelScope
5+
import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionsUseCase
6+
import com.threegap.bitnagil.domain.emotion.usecase.RegisterEmotionUseCase
7+
import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel
8+
import com.threegap.bitnagil.presentation.emotion.model.Emotion
9+
import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionIntent
10+
import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionSideEffect
11+
import com.threegap.bitnagil.presentation.emotion.model.mvi.EmotionState
12+
import dagger.hilt.android.lifecycle.HiltViewModel
13+
import kotlinx.coroutines.launch
14+
import org.orbitmvi.orbit.syntax.simple.SimpleSyntax
15+
import org.orbitmvi.orbit.syntax.simple.postSideEffect
16+
import javax.inject.Inject
17+
18+
@HiltViewModel
19+
class EmotionViewModel @Inject constructor(
20+
savedStateHandle: SavedStateHandle,
21+
private val getEmotionsUseCase: GetEmotionsUseCase,
22+
private val registerEmotionUseCase: RegisterEmotionUseCase,
23+
) : MviViewModel<EmotionState, EmotionSideEffect, EmotionIntent>(
24+
savedStateHandle = savedStateHandle,
25+
initState = EmotionState.Init
26+
) {
27+
init {
28+
loadEmotions()
29+
}
30+
31+
private fun loadEmotions() {
32+
viewModelScope.launch {
33+
getEmotionsUseCase().fold(
34+
onSuccess = { emotions ->
35+
sendIntent(
36+
EmotionIntent.EmotionListLoadSuccess(emotions = emotions.map { Emotion.fromDomain(it) })
37+
)
38+
},
39+
onFailure = {
40+
// todo 실패 케이스 정의되면 처리
41+
},
42+
)
43+
}
44+
}
45+
46+
override suspend fun SimpleSyntax<EmotionState, EmotionSideEffect>.reduceState(intent: EmotionIntent, state: EmotionState): EmotionState? {
47+
when(intent) {
48+
is EmotionIntent.EmotionListLoadSuccess -> {
49+
return state.copy(
50+
emotions = intent.emotions,
51+
isLoading = false,
52+
)
53+
}
54+
EmotionIntent.RegisterEmotionSuccess -> {
55+
postSideEffect(EmotionSideEffect.NavigateToBack)
56+
return null
57+
}
58+
}
59+
}
60+
61+
fun selectEmotion(emotion: Emotion) {
62+
viewModelScope.launch {
63+
registerEmotionUseCase(emotion = emotion.toDomain()).fold(
64+
onSuccess = {
65+
sendIntent(EmotionIntent.RegisterEmotionSuccess)
66+
},
67+
onFailure = {
68+
// todo 실패 케이스 정의되면 처리
69+
}
70+
)
71+
}
72+
}
73+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.threegap.bitnagil.presentation.emotion.model
2+
3+
import com.threegap.bitnagil.domain.emotion.model.Emotion as DomainEmotion
4+
5+
enum class Emotion(
6+
val emotionName: String,
7+
val imageResourceId: Int,
8+
) {
9+
// todo - 이미지 리소스 전달받은 후 해당 이미지로 수정
10+
CALM(emotionName = "평온함", imageResourceId = android.R.drawable.ic_menu_report_image),
11+
VITALITY(emotionName = "활기참", imageResourceId = android.R.drawable.ic_menu_report_image),
12+
LETHARGY(emotionName = "무기력함", imageResourceId = android.R.drawable.ic_menu_report_image),
13+
ANXIETY(emotionName = "불안함", imageResourceId = android.R.drawable.ic_menu_report_image),
14+
SATISFACTION(emotionName = "만족함", imageResourceId = android.R.drawable.ic_menu_report_image),
15+
FATIGUE(emotionName = "피로함", imageResourceId = android.R.drawable.ic_menu_report_image),
16+
;
17+
18+
companion object {
19+
fun fromDomain(domain: DomainEmotion): Emotion {
20+
return when (domain) {
21+
DomainEmotion.CALM -> CALM
22+
DomainEmotion.VITALITY -> VITALITY
23+
DomainEmotion.LETHARGY -> LETHARGY
24+
DomainEmotion.ANXIETY -> ANXIETY
25+
DomainEmotion.SATISFACTION -> SATISFACTION
26+
DomainEmotion.FATIGUE -> FATIGUE
27+
}
28+
}
29+
}
30+
31+
fun toDomain(): DomainEmotion {
32+
return when (this) {
33+
CALM -> DomainEmotion.CALM
34+
VITALITY -> DomainEmotion.VITALITY
35+
LETHARGY -> DomainEmotion.LETHARGY
36+
ANXIETY -> DomainEmotion.ANXIETY
37+
SATISFACTION -> DomainEmotion.SATISFACTION
38+
FATIGUE -> DomainEmotion.FATIGUE
39+
}
40+
}
41+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.threegap.bitnagil.presentation.emotion.model.mvi
2+
3+
import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent
4+
import com.threegap.bitnagil.presentation.emotion.model.Emotion
5+
6+
sealed class EmotionIntent: MviIntent {
7+
data class EmotionListLoadSuccess(val emotions: List<Emotion>) : EmotionIntent()
8+
data object RegisterEmotionSuccess : EmotionIntent()
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.threegap.bitnagil.presentation.emotion.model.mvi
2+
3+
import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect
4+
5+
sealed class EmotionSideEffect : MviSideEffect {
6+
data object NavigateToBack : EmotionSideEffect()
7+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.threegap.bitnagil.presentation.emotion.model.mvi
2+
3+
import androidx.compose.runtime.Immutable
4+
import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState
5+
import com.threegap.bitnagil.presentation.emotion.model.Emotion
6+
import kotlinx.parcelize.Parcelize
7+
8+
@Parcelize
9+
@Immutable
10+
data class EmotionState(
11+
val emotions: List<Emotion>,
12+
val isLoading: Boolean,
13+
) : MviState {
14+
companion object {
15+
val Init = EmotionState(
16+
emotions = emptyList(),
17+
isLoading = true,
18+
)
19+
}
20+
}

0 commit comments

Comments
 (0)