Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
Expand All @@ -34,41 +33,52 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.alarmy.near.R
import com.alarmy.near.presentation.feature.onboarding.components.BackgroundArea
import com.alarmy.near.presentation.feature.onboarding.components.OnboardingButton
import com.alarmy.near.presentation.feature.onboarding.components.PageIndicator
import com.alarmy.near.presentation.feature.onboarding.model.OnboardingPage
import com.alarmy.near.presentation.preview.DevicePreviewParameterProvider
import com.alarmy.near.presentation.preview.component.DevicePreviewFrame
import com.alarmy.near.presentation.preview.model.DevicePreviewSpec
import com.alarmy.near.presentation.ui.component.NearFrame
import com.alarmy.near.presentation.ui.theme.NearTheme
import kotlinx.coroutines.launch

/**
* 온보딩 화면 메인 컴포넌트
* 5페이지로 구성된 뷰페이저 형태의 온보딩 화면
*/
@Composable
fun OnboardingScreen(
fun OnboardingRoute(
onNavigateToLogin: () -> Unit,
viewModel: OnboardingViewModel = hiltViewModel(),
) {
// UI 상태 관찰
val uiState by viewModel.uiState.collectAsState()
val uiState = viewModel.uiState.collectAsStateWithLifecycle()

// 사이드 이펙트 처리
LaunchedEffect(Unit) {
viewModel.effect.collect { effect ->
when (effect) {
is OnboardingEffect.NavigateToLogin -> {
onNavigateToLogin()
}
is OnboardingEffect.NavigateToLogin -> onNavigateToLogin()
}
}
}

OnboardingScreen(
state = uiState.value,
onCompleteOnboarding = { viewModel.completeOnboarding() },
)
}

/**
* 온보딩 화면 메인 컴포넌트
* 5페이지로 구성된 뷰페이저 형태의 온보딩 화면
*/
@Composable
fun OnboardingScreen(
state: OnboardingUiState,
onCompleteOnboarding: () -> Unit = {},
) {
// 온보딩 페이지 데이터 - remember로 성능 최적화
val pages =
remember {
Expand Down Expand Up @@ -99,61 +109,63 @@ fun OnboardingScreen(
// 상태바와 네비게이션 바 높이 계산
val density = LocalDensity.current
val statusBarHeightDp = with(density) { WindowInsets.statusBars.getTop(density).toDp() }
val navigationBarHeightDp = with(density) { WindowInsets.navigationBars.getBottom(density).toDp() }
val navigationBarHeightDp =
with(density) { WindowInsets.navigationBars.getBottom(density).toDp() }

// 페이저 상태 관리
val pagerState = rememberPagerState(pageCount = { pages.size })
val scope = rememberCoroutineScope()

NearFrame {
Box(
modifier = Modifier.fillMaxSize(),
) {
BackgroundArea()
NearFrame(applySystemBarsPadding = false) {
Box(modifier = Modifier.fillMaxSize()) {
Image(
modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.onboarding_bg_img),
contentDescription = null,
contentScale = ContentScale.FillBounds,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

배경 이미지에 ContentScale.FillBounds를 사용하면 기기 화면 비율에 따라 이미지가 왜곡되어 보일 수 있습니다. 이미지의 비율을 유지하면서 화면을 채우려면 ContentScale.Crop을 사용하는 것이 좋습니다. Crop은 이미지가 잘릴 수는 있지만, 왜곡되지 않아 더 자연스러운 배경을 제공할 수 있습니다.

Suggested change
contentScale = ContentScale.FillBounds,
contentScale = ContentScale.Crop,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini의 리뷰대로 Crop은 이미지가 잘릴 수도 있다는 점에서 수정하지 않고 FillBounds로 남겨두겠습니다.

)
Column(
modifier =
Modifier
.fillMaxSize()
.padding(top = statusBarHeightDp, bottom = navigationBarHeightDp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// 뷰페이저
HorizontalPager(
state = pagerState,
) { page ->
OnboardingPageContent(
page = pages[page],
modifier = Modifier.fillMaxWidth(),
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
HorizontalPager(
modifier = Modifier.weight(1f),
state = pagerState,
) { page ->
OnboardingPageContent(
page = pages[page],
modifier = Modifier.fillMaxSize(),
)
}
PageIndicator(
pageCount = pages.size,
currentPage = pagerState.currentPage,
)
}

Spacer(modifier = Modifier.size(24.dp))

// 페이지 인디케이터
PageIndicator(
pageCount = pages.size,
currentPage = pagerState.currentPage,
)

Spacer(modifier = Modifier.size(14.dp))
}
Column(modifier = Modifier.align(Alignment.BottomCenter)) {
// 다음/완료 버튼
OnboardingButton(
currentPage = pagerState.currentPage,
totalPages = pages.size,
isLoading = uiState.isLoading,
onNextClick = {
if (pagerState.currentPage < pages.size - 1) {
scope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
OnboardingButton(
currentPage = pagerState.currentPage,
totalPages = pages.size,
isLoading = state.isLoading,
onNextClick = {
if (pagerState.currentPage < pages.size - 1) {
scope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
} else {
onCompleteOnboarding()
}
} else {
// 온보딩 완료 시 DataStore에 저장
viewModel.completeOnboarding()
}
},
)
Spacer(modifier = Modifier.size(24.dp))
},
)
Spacer(modifier = Modifier.size(24.dp))
}
}
}
}
Expand All @@ -172,7 +184,7 @@ private fun OnboardingPageContent(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.size(22.dp))
Spacer(modifier = Modifier.size(44.dp))

// 각 온보딩 페이지 타이틀
Text(
Expand All @@ -184,16 +196,17 @@ private fun OnboardingPageContent(
lineHeight = 30.sp,
),
)

Spacer(modifier = Modifier.size(16.dp))

Image(
modifier =
Modifier
.weight(1f)
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp)),
painter = painterResource(page.image),
contentDescription = null,
)
Spacer(modifier = Modifier.size(16.dp))
}
}

Expand Down Expand Up @@ -235,10 +248,12 @@ private fun AnnotatedString.Builder.appendStyledText(

@Preview(showBackground = true)
@Composable
fun OnboardingScreenPreview() {
NearTheme {
fun OnboardingScreenPreview(
@PreviewParameter(DevicePreviewParameterProvider::class) spec: DevicePreviewSpec,
) {
DevicePreviewFrame(spec = spec) {
OnboardingScreen(
onNavigateToLogin = {},
state = OnboardingUiState(),
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.alarmy.near.presentation.feature.onboarding.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.alarmy.near.presentation.feature.onboarding.OnboardingScreen
import com.alarmy.near.presentation.feature.onboarding.OnboardingRoute
import kotlinx.serialization.Serializable

@Serializable
Expand All @@ -19,7 +19,7 @@ fun NavController.navigateToOnboarding() {
// 온보딩 네비게이션 그래프
fun NavGraphBuilder.onboardingNavGraph(onNavigateToLogin: () -> Unit) {
composable<RouteOnboarding> {
OnboardingScreen(
OnboardingRoute(
onNavigateToLogin = onNavigateToLogin,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.alarmy.near.presentation.preview

import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.alarmy.near.presentation.preview.model.DevicePreviewSpec

/**
* 삼성 대표 기기 해상도를 기반으로 Compose 프리뷰에서 사용할 dp 값을 제공합니다.
*/
class DevicePreviewParameterProvider : PreviewParameterProvider<DevicePreviewSpec> {
override val values: Sequence<DevicePreviewSpec> =
sequenceOf(
DevicePreviewSpec(
deviceName = "Galaxy S21 (FHD+)",
widthDp = 360,
heightDp = 800,
),
DevicePreviewSpec(
deviceName = "Galaxy S23 (FHD+)",
widthDp = 360,
heightDp = 780,
),
DevicePreviewSpec(
deviceName = "Galaxy S25 (QHD+)",
widthDp = 412,
heightDp = 915,
),
DevicePreviewSpec(
deviceName = "Galaxy S23 Ultra (QHD+)",
widthDp = 411,
heightDp = 915,
),
DevicePreviewSpec(
deviceName = "Galaxy Z Flip (FHD+)",
widthDp = 360,
heightDp = 860,
),
DevicePreviewSpec(
deviceName = "Galaxy Z Fold (QXGA+)",
widthDp = 600,
heightDp = 730,
),
DevicePreviewSpec(
deviceName = "Galaxy A60 (FHD+)",
widthDp = 360,
heightDp = 780,
),
)
}
Loading