Skip to content

Commit be6ccc4

Browse files
authored
[Feat/#1585] SP2 솝레터 온보딩 및 이름 설정 UI (#1590)
* [FEAT#1585] 솝레터 온보딩 스크린 및 내비게이션 구현 - `SopletterOnboardingNavigation`을 추가하여 온보딩 화면으로의 경로 정의 및 이동 함수 구현 - `SopletterOnboardingScreen` 및 `SopletterOnboardingRoute` 구현 - `OnboardingInfoHolder`와 `SopletterButton`을 포함한 온보딩 UI 레이아웃 구성 * [FEAT#1585] 솝레터 온보딩 및 닉네임 확인 화면 구현 - `SopletterOnboardingScreen` 및 `OnboardingInfoHolder` 컴포넌트 추가 - `SopletterNameScreen` 및 `NameInfoHolder` 닉네임 확인 UI 추가 - `SopletterNameViewModel`, `NameState`, `NameSideEffect` 등 MVI 기반 로직 구현 - `SopletterNavigation`을 통한 온보딩 네비게이션 그래프 정의 * [FEAT#1585] 솝레터 관련 공통 버튼 컴포넌트 및 리소스 추가 - 공통 버튼 컴포넌트 `SopletterButton` 구현 - 화면에 사용되는 이미지 리소스 추가 (`img_sopletter_mailbox`, `img_sopletter_onboarding_title`, `img_sopletter_name_letter`) * [REFACTOR#1585] Sopletter 관련 컴포넌트 및 함수 네이밍 통일 및 오타 수정 - `NameRoute`, `NameScreen` 등 관련 함수명을 `SopletterNameRoute`, `SopletterNameScreen`으로 변경하여 일관성 확보 - `NameInfoHolder` 및 `OnboardingInfoHolder`를 각각 `SopletterNameInfoHolder`, `SopletterOnboardingInfoHolder`로 클래스명 변경 및 파일 리네임 - `SopletterOnboadingScreen.kt` 파일명의 오타를 `SopletterOnboardingScreen.kt`로 수정 - `SopletterNameScreen` 내 버튼 텍스트의 오타 수정 ("바로가가" -> "바로가기")
1 parent 262dbd0 commit be6ccc4

12 files changed

Lines changed: 651 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.sopt.official.sopletter.component
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.shape.RoundedCornerShape
9+
import androidx.compose.material3.Text
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Alignment
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.draw.clip
14+
import androidx.compose.ui.tooling.preview.Preview
15+
import androidx.compose.ui.unit.dp
16+
import org.sopt.official.designsystem.SoptTheme
17+
18+
@Composable
19+
internal fun SopletterButton(
20+
buttonText: String,
21+
onClick: () -> Unit,
22+
modifier: Modifier = Modifier
23+
) {
24+
Box(
25+
modifier = modifier
26+
.fillMaxWidth()
27+
.clip(shape = RoundedCornerShape(12.dp))
28+
.background(color = SoptTheme.colors.primary)
29+
.clickable(onClick = onClick),
30+
contentAlignment = Alignment.Center
31+
) {
32+
Text(
33+
text = buttonText,
34+
style = SoptTheme.typography.label18SB,
35+
color = SoptTheme.colors.onSurface,
36+
modifier = Modifier
37+
.padding(vertical = 16.dp)
38+
)
39+
}
40+
}
41+
42+
@Preview
43+
@Composable
44+
private fun SopletterButtonPreview() {
45+
SoptTheme {
46+
SopletterButton(
47+
buttonText = "솝레터 시작하기",
48+
onClick = {}
49+
)
50+
}
51+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.sopt.official.sopletter.name
2+
3+
import androidx.compose.runtime.Immutable
4+
5+
// Todo : 추후 서버 반영
6+
@Immutable
7+
data class NameState(
8+
val name : String = "",
9+
val generation : Int = 0
10+
)
11+
12+
sealed interface NameSideEffect {
13+
data object NavigateToSopletterMain : NameSideEffect
14+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package org.sopt.official.sopletter.name
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.LaunchedEffect
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.tooling.preview.Preview
14+
import androidx.compose.ui.unit.dp
15+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
16+
import androidx.lifecycle.compose.LocalLifecycleOwner
17+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
18+
import androidx.lifecycle.flowWithLifecycle
19+
import org.sopt.official.designsystem.SoptTheme
20+
import org.sopt.official.sopletter.component.SopletterButton
21+
import org.sopt.official.sopletter.name.component.SopletterNameInfoHolder
22+
23+
@Composable
24+
fun SopletterNameRoute(
25+
viewModel: SopletterNameViewModel = hiltViewModel()
26+
) {
27+
val lifeCycleOwner = LocalLifecycleOwner.current
28+
29+
val state by viewModel.state.collectAsStateWithLifecycle()
30+
31+
LaunchedEffect(Unit) {
32+
viewModel.sideEffect.flowWithLifecycle(lifeCycleOwner.lifecycle)
33+
.collect { sideEffect ->
34+
when (sideEffect) {
35+
NameSideEffect.NavigateToSopletterMain -> { /* Todo : SopltterMain 네비게이션 연결 */}
36+
}
37+
}
38+
}
39+
40+
41+
SopletterNameScreen(
42+
state = state
43+
)
44+
}
45+
46+
@Composable
47+
private fun SopletterNameScreen(
48+
state: NameState,
49+
navigateToSopletterMain: () -> Unit= {}
50+
) {
51+
Column(
52+
modifier = Modifier
53+
.fillMaxSize()
54+
.background(SoptTheme.colors.onSurface950)
55+
) {
56+
Spacer(modifier = Modifier.weight(20f))
57+
58+
SopletterNameInfoHolder(
59+
info = state,
60+
modifier = Modifier
61+
.fillMaxWidth()
62+
.padding(horizontal = 20.dp)
63+
)
64+
65+
Spacer(modifier = Modifier.weight(228f))
66+
67+
SopletterButton(
68+
buttonText = "${state.generation}기 솝레터 바로가기",
69+
onClick = navigateToSopletterMain,
70+
modifier = Modifier
71+
.fillMaxWidth()
72+
.padding(horizontal = 20.dp)
73+
)
74+
75+
Spacer(modifier = Modifier.weight(40f))
76+
77+
}
78+
}
79+
80+
@Preview
81+
@Composable
82+
private fun SopletterNameScreenPreview() {
83+
SoptTheme {
84+
SopletterNameScreen(
85+
state = NameState(
86+
name = "익명의 김솝트",
87+
generation = 38
88+
),
89+
navigateToSopletterMain = {}
90+
)
91+
}
92+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.sopt.official.sopletter.name
2+
3+
import androidx.lifecycle.ViewModel
4+
import dagger.hilt.android.lifecycle.HiltViewModel
5+
import kotlinx.coroutines.flow.MutableSharedFlow
6+
import kotlinx.coroutines.flow.MutableStateFlow
7+
import kotlinx.coroutines.flow.asSharedFlow
8+
import kotlinx.coroutines.flow.asStateFlow
9+
import javax.inject.Inject
10+
11+
@HiltViewModel
12+
class SopletterNameViewModel @Inject constructor(
13+
14+
) : ViewModel() {
15+
private val _state = MutableStateFlow(NameState())
16+
val state = _state.asStateFlow()
17+
18+
private val _sideEffect = MutableSharedFlow<NameSideEffect>()
19+
val sideEffect = _sideEffect.asSharedFlow()
20+
21+
22+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package org.sopt.official.sopletter.name.component
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.Column
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.foundation.shape.RoundedCornerShape
11+
import androidx.compose.material3.HorizontalDivider
12+
import androidx.compose.material3.Text
13+
import androidx.compose.runtime.Composable
14+
import androidx.compose.ui.Alignment
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.graphics.vector.ImageVector
17+
import androidx.compose.ui.res.vectorResource
18+
import androidx.compose.ui.text.style.TextAlign
19+
import androidx.compose.ui.tooling.preview.Preview
20+
import androidx.compose.ui.unit.dp
21+
import org.sopt.official.designsystem.SoptTheme
22+
import org.sopt.official.sopletter.R
23+
import org.sopt.official.sopletter.name.NameState
24+
25+
@Composable
26+
internal fun SopletterNameInfoHolder(
27+
info: NameState,
28+
modifier: Modifier = Modifier
29+
) {
30+
Column (
31+
modifier = modifier
32+
.background(
33+
color = SoptTheme.colors.onSurface800,
34+
shape = RoundedCornerShape(12.dp)
35+
),
36+
horizontalAlignment = Alignment.CenterHorizontally
37+
) {
38+
Spacer(modifier = Modifier.height(28.dp))
39+
40+
Image(
41+
imageVector = ImageVector.vectorResource(id = R.drawable.img_sopletter_name_letter),
42+
contentDescription = null,
43+
)
44+
45+
Spacer(modifier = Modifier.height(20.dp))
46+
47+
Text(
48+
text = "${info.generation}기 솝레터에 입장할 준비 되셨나요?\n" +
49+
"솝레터는 100% 익명이에요.",
50+
style = SoptTheme.typography.title18SB,
51+
color = SoptTheme.colors.onSurface50,
52+
modifier = Modifier
53+
.padding(horizontal = 30.dp),
54+
textAlign = TextAlign.Center
55+
)
56+
57+
Spacer(modifier = Modifier.height(16.dp))
58+
59+
HorizontalDivider(
60+
thickness = 1.dp,
61+
color = SoptTheme.colors.onSurface600,
62+
modifier = Modifier
63+
.fillMaxWidth()
64+
.padding(horizontal = 5.dp)
65+
)
66+
67+
Spacer(modifier = Modifier.height(16.dp))
68+
69+
Text(
70+
text = "나의 닉네임은",
71+
style = SoptTheme.typography.title20SB,
72+
color = SoptTheme.colors.onSurface300,
73+
)
74+
75+
Text(
76+
text = info.name,
77+
style = SoptTheme.typography.title24SB,
78+
color = SoptTheme.colors.onSurface30,
79+
)
80+
81+
Spacer(modifier = Modifier.height(34.dp))
82+
}
83+
}
84+
85+
@Preview
86+
@Composable
87+
private fun NameInfoHolderPreview() {
88+
SoptTheme {
89+
SopletterNameInfoHolder(
90+
info = NameState(
91+
name = "익명의 김솝트",
92+
generation = 37
93+
)
94+
)
95+
}
96+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.sopt.official.sopletter.navigation
2+
3+
import androidx.compose.foundation.layout.PaddingValues
4+
import androidx.navigation.NavController
5+
import androidx.navigation.NavGraphBuilder
6+
import androidx.navigation.NavOptions
7+
import androidx.navigation.compose.composable
8+
import androidx.navigation.navigation
9+
import kotlinx.serialization.Serializable
10+
import org.sopt.official.sopletter.onboarding.SopletterOnboardingRoute
11+
import org.sopt.official.sopletter.onboarding.navigation.SopletterOnboarding
12+
13+
@Serializable
14+
data object SopletterGraph
15+
16+
fun NavGraphBuilder.sopletterGraph(
17+
paddingValues: PaddingValues,
18+
navController: NavController,
19+
navigateUp: () -> Unit,
20+
) {
21+
navigation<SopletterGraph> (
22+
startDestination = SopletterOnboarding
23+
) {
24+
composable<SopletterOnboarding> {
25+
SopletterOnboardingRoute (
26+
paddingValues = paddingValues,
27+
navigateToNickname = {}
28+
)
29+
}
30+
}
31+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.sopt.official.sopletter.onboarding
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.PaddingValues
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.tooling.preview.Preview
13+
import androidx.compose.ui.unit.dp
14+
import org.sopt.official.designsystem.SoptTheme
15+
import org.sopt.official.sopletter.component.SopletterButton
16+
import org.sopt.official.sopletter.onboarding.component.SopletterOnboardingInfoHolder
17+
18+
@Composable
19+
fun SopletterOnboardingRoute(
20+
paddingValues: PaddingValues,
21+
navigateToNickname: () -> Unit,
22+
) {
23+
SopletterOnboardingScreen(
24+
paddingValues = paddingValues,
25+
navigateToNickname = navigateToNickname,
26+
)
27+
}
28+
29+
@Composable
30+
private fun SopletterOnboardingScreen(
31+
paddingValues: PaddingValues,
32+
navigateToNickname: () -> Unit,
33+
) {
34+
Column(
35+
modifier = Modifier
36+
.fillMaxSize()
37+
.background(SoptTheme.colors.onSurface950)
38+
.padding(paddingValues),
39+
) {
40+
// Todo : 탑바
41+
42+
Spacer(modifier = Modifier.weight(58f))
43+
44+
SopletterOnboardingInfoHolder(
45+
modifier = Modifier
46+
.fillMaxWidth()
47+
.padding(horizontal = 25.dp)
48+
)
49+
50+
Spacer(modifier = Modifier.weight(140f))
51+
52+
SopletterButton(
53+
buttonText = "솝레터 시작하기",
54+
onClick = navigateToNickname,
55+
modifier = Modifier
56+
.padding(horizontal = 20.dp)
57+
)
58+
59+
Spacer(modifier = Modifier.weight(40f))
60+
}
61+
}
62+
63+
@Preview
64+
@Composable
65+
private fun SopletterOnboardingScreenPreview() {
66+
SoptTheme {
67+
SopletterOnboardingScreen(
68+
paddingValues = PaddingValues(),
69+
navigateToNickname = {},
70+
)
71+
}
72+
}

0 commit comments

Comments
 (0)