@@ -17,14 +17,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape
1717import androidx.compose.material3.Text
1818import androidx.compose.runtime.Composable
1919import androidx.compose.runtime.LaunchedEffect
20- import androidx.compose.runtime.collectAsState
21- import androidx.compose.runtime.getValue
2220import androidx.compose.runtime.remember
2321import androidx.compose.runtime.rememberCoroutineScope
2422import androidx.compose.ui.Alignment
2523import androidx.compose.ui.Modifier
2624import androidx.compose.ui.draw.clip
2725import androidx.compose.ui.graphics.Color
26+ import androidx.compose.ui.layout.ContentScale
2827import androidx.compose.ui.platform.LocalDensity
2928import androidx.compose.ui.res.painterResource
3029import androidx.compose.ui.res.stringResource
@@ -34,41 +33,52 @@ import androidx.compose.ui.text.buildAnnotatedString
3433import androidx.compose.ui.text.style.TextAlign
3534import androidx.compose.ui.text.withStyle
3635import androidx.compose.ui.tooling.preview.Preview
36+ import androidx.compose.ui.tooling.preview.PreviewParameter
3737import androidx.compose.ui.unit.dp
3838import androidx.compose.ui.unit.sp
3939import androidx.hilt.navigation.compose.hiltViewModel
40+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
4041import com.alarmy.near.R
41- import com.alarmy.near.presentation.feature.onboarding.components.BackgroundArea
4242import com.alarmy.near.presentation.feature.onboarding.components.OnboardingButton
4343import com.alarmy.near.presentation.feature.onboarding.components.PageIndicator
4444import com.alarmy.near.presentation.feature.onboarding.model.OnboardingPage
45+ import com.alarmy.near.presentation.preview.DevicePreviewParameterProvider
46+ import com.alarmy.near.presentation.preview.component.DevicePreviewFrame
47+ import com.alarmy.near.presentation.preview.model.DevicePreviewSpec
4548import com.alarmy.near.presentation.ui.component.NearFrame
4649import com.alarmy.near.presentation.ui.theme.NearTheme
4750import kotlinx.coroutines.launch
4851
49- /* *
50- * 온보딩 화면 메인 컴포넌트
51- * 5페이지로 구성된 뷰페이저 형태의 온보딩 화면
52- */
5352@Composable
54- fun OnboardingScreen (
53+ fun OnboardingRoute (
5554 onNavigateToLogin : () -> Unit ,
5655 viewModel : OnboardingViewModel = hiltViewModel(),
5756) {
58- // UI 상태 관찰
59- val uiState by viewModel.uiState.collectAsState()
57+ val uiState = viewModel.uiState.collectAsStateWithLifecycle()
6058
61- // 사이드 이펙트 처리
6259 LaunchedEffect (Unit ) {
6360 viewModel.effect.collect { effect ->
6461 when (effect) {
65- is OnboardingEffect .NavigateToLogin -> {
66- onNavigateToLogin()
67- }
62+ is OnboardingEffect .NavigateToLogin -> onNavigateToLogin()
6863 }
6964 }
7065 }
7166
67+ OnboardingScreen (
68+ state = uiState.value,
69+ onCompleteOnboarding = { viewModel.completeOnboarding() },
70+ )
71+ }
72+
73+ /* *
74+ * 온보딩 화면 메인 컴포넌트
75+ * 5페이지로 구성된 뷰페이저 형태의 온보딩 화면
76+ */
77+ @Composable
78+ fun OnboardingScreen (
79+ state : OnboardingUiState ,
80+ onCompleteOnboarding : () -> Unit = {},
81+ ) {
7282 // 온보딩 페이지 데이터 - remember로 성능 최적화
7383 val pages =
7484 remember {
@@ -99,61 +109,63 @@ fun OnboardingScreen(
99109 // 상태바와 네비게이션 바 높이 계산
100110 val density = LocalDensity .current
101111 val statusBarHeightDp = with (density) { WindowInsets .statusBars.getTop(density).toDp() }
102- val navigationBarHeightDp = with (density) { WindowInsets .navigationBars.getBottom(density).toDp() }
112+ val navigationBarHeightDp =
113+ with (density) { WindowInsets .navigationBars.getBottom(density).toDp() }
103114
104115 // 페이저 상태 관리
105116 val pagerState = rememberPagerState(pageCount = { pages.size })
106117 val scope = rememberCoroutineScope()
107118
108- NearFrame {
109- Box (
110- modifier = Modifier .fillMaxSize(),
111- ) {
112- BackgroundArea ()
119+ NearFrame (applySystemBarsPadding = false ) {
120+ Box (modifier = Modifier .fillMaxSize()) {
121+ Image (
122+ modifier = Modifier .fillMaxSize(),
123+ painter = painterResource(R .drawable.onboarding_bg_img),
124+ contentDescription = null ,
125+ contentScale = ContentScale .FillBounds ,
126+ )
113127 Column (
114128 modifier =
115129 Modifier
130+ .fillMaxSize()
116131 .padding(top = statusBarHeightDp, bottom = navigationBarHeightDp),
117- horizontalAlignment = Alignment .CenterHorizontally ,
118132 ) {
119- // 뷰페이저
120- HorizontalPager (
121- state = pagerState,
122- ) { page ->
123- OnboardingPageContent (
124- page = pages[page],
125- modifier = Modifier .fillMaxWidth(),
133+ Column (
134+ modifier = Modifier .weight(1f ),
135+ horizontalAlignment = Alignment .CenterHorizontally ,
136+ ) {
137+ HorizontalPager (
138+ modifier = Modifier .weight(1f ),
139+ state = pagerState,
140+ ) { page ->
141+ OnboardingPageContent (
142+ page = pages[page],
143+ modifier = Modifier .fillMaxSize(),
144+ )
145+ }
146+ PageIndicator (
147+ pageCount = pages.size,
148+ currentPage = pagerState.currentPage,
126149 )
127150 }
128-
129151 Spacer (modifier = Modifier .size(24 .dp))
130-
131- // 페이지 인디케이터
132- PageIndicator (
133- pageCount = pages.size,
134- currentPage = pagerState.currentPage,
135- )
136-
137- Spacer (modifier = Modifier .size(14 .dp))
138- }
139- Column (modifier = Modifier .align(Alignment .BottomCenter )) {
140- // 다음/완료 버튼
141- OnboardingButton (
142- currentPage = pagerState.currentPage,
143- totalPages = pages.size,
144- isLoading = uiState.isLoading,
145- onNextClick = {
146- if (pagerState.currentPage < pages.size - 1 ) {
147- scope.launch {
148- pagerState.animateScrollToPage(pagerState.currentPage + 1 )
152+ Column (horizontalAlignment = Alignment .CenterHorizontally ) {
153+ OnboardingButton (
154+ currentPage = pagerState.currentPage,
155+ totalPages = pages.size,
156+ isLoading = state.isLoading,
157+ onNextClick = {
158+ if (pagerState.currentPage < pages.size - 1 ) {
159+ scope.launch {
160+ pagerState.animateScrollToPage(pagerState.currentPage + 1 )
161+ }
162+ } else {
163+ onCompleteOnboarding()
149164 }
150- } else {
151- // 온보딩 완료 시 DataStore에 저장
152- viewModel.completeOnboarding()
153- }
154- },
155- )
156- Spacer (modifier = Modifier .size(24 .dp))
165+ },
166+ )
167+ Spacer (modifier = Modifier .size(24 .dp))
168+ }
157169 }
158170 }
159171 }
@@ -172,7 +184,7 @@ private fun OnboardingPageContent(
172184 modifier = modifier,
173185 horizontalAlignment = Alignment .CenterHorizontally ,
174186 ) {
175- Spacer (modifier = Modifier .size(22 .dp))
187+ Spacer (modifier = Modifier .size(44 .dp))
176188
177189 // 각 온보딩 페이지 타이틀
178190 Text (
@@ -184,16 +196,17 @@ private fun OnboardingPageContent(
184196 lineHeight = 30 .sp,
185197 ),
186198 )
187-
188199 Spacer (modifier = Modifier .size(16 .dp))
189-
190200 Image (
191201 modifier =
192202 Modifier
203+ .weight(1f )
204+ .fillMaxWidth()
193205 .clip(RoundedCornerShape (12 .dp)),
194206 painter = painterResource(page.image),
195207 contentDescription = null ,
196208 )
209+ Spacer (modifier = Modifier .size(16 .dp))
197210 }
198211}
199212
@@ -235,10 +248,12 @@ private fun AnnotatedString.Builder.appendStyledText(
235248
236249@Preview(showBackground = true )
237250@Composable
238- fun OnboardingScreenPreview () {
239- NearTheme {
251+ fun OnboardingScreenPreview (
252+ @PreviewParameter(DevicePreviewParameterProvider ::class ) spec : DevicePreviewSpec ,
253+ ) {
254+ DevicePreviewFrame (spec = spec) {
240255 OnboardingScreen (
241- onNavigateToLogin = {} ,
256+ state = OnboardingUiState () ,
242257 )
243258 }
244259}
0 commit comments