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
10 changes: 8 additions & 2 deletions app/src/main/java/com/example/hangsha_android/ui/HangshaApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ fun HangshaApp() {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val showBottomBar =
val isOnboardingInterestPriority =
currentDestination?.route == HangshaDestinations.InterestPriority.route &&
navBackStackEntry?.arguments
?.getString(HangshaDestinations.InterestPriority.sourceArg) ==
HangshaDestinations.InterestPriority.sourceOnboarding
val isMainGraphDestination =
currentDestination?.hierarchy?.any { it.route == HangshaDestinations.Main.route } == true
val showBottomBar = isMainGraphDestination && !isOnboardingInterestPriority

Scaffold(
containerColor = MaterialTheme.colorScheme.background,
Expand All @@ -43,4 +49,4 @@ fun HangshaApp() {
innerPadding = innerPadding
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import com.example.hangsha_android.ui.view.mymemos.MyMemosScreen
import com.example.hangsha_android.ui.view.mymemos.MyMemosViewModel
import com.example.hangsha_android.ui.view.onboarding.OnboardingScreen
import com.example.hangsha_android.ui.view.onboarding.OnboardingViewModel
import com.example.hangsha_android.ui.view.onboarding.OnboardingWelcomeScreen
import com.example.hangsha_android.ui.view.signup.SignUpScreen
import com.example.hangsha_android.ui.view.signup.SignUpViewModel
import com.google.android.gms.auth.api.signin.GoogleSignIn
Expand All @@ -64,8 +65,18 @@ sealed class HangshaDestinations(val route: String) {
data object CredentialLogin : HangshaDestinations("credential_login")
data object SignUp : HangshaDestinations("sign_up")
data object Onboarding : HangshaDestinations("onboarding")
data object OnboardingWelcome : HangshaDestinations("onboarding_welcome")
data object Main : HangshaDestinations("main")
data object InterestPriority : HangshaDestinations("interest_priority")
data object InterestPriority : HangshaDestinations("interest_priority?source={source}") {
const val baseRoute = "interest_priority"
const val sourceArg = "source"
const val sourceMyPage = "mypage"
const val sourceOnboarding = "onboarding"

fun createRoute(source: String = sourceMyPage): String {
return "$baseRoute?$sourceArg=$source"
}
}
data object MyBookmarks : HangshaDestinations("my_bookmarks")
data object MyMemos : HangshaDestinations("my_memos")
data object DailyEvents : HangshaDestinations("daily_events/{date}") {
Expand Down Expand Up @@ -254,22 +265,50 @@ fun NavGraphBuilder.onboardingGraph(navController: NavHostController) {
onboardingViewModel.onOnboardingMessageConsumed()
}

LaunchedEffect(onboardingUiState.isUsernameSaved) {
if (!onboardingUiState.isUsernameSaved) {
LaunchedEffect(onboardingUiState.isProfileSaved) {
if (!onboardingUiState.isProfileSaved) {
return@LaunchedEffect
}

// TODO(ONBOARDING): Add the remaining onboarding steps after username setup.
navController.navigate(HangshaDestinations.Main.route) {
popUpTo(HangshaDestinations.Onboarding.route) { inclusive = true }
}
onboardingViewModel.onUsernameSavedConsumed()
navController.navigate(
HangshaDestinations.InterestPriority.createRoute(
source = HangshaDestinations.InterestPriority.sourceOnboarding
)
)
onboardingViewModel.onProfileSavedConsumed()
}

OnboardingScreen(
uiState = onboardingUiState,
onUsernameChanged = { value -> onboardingViewModel.onUsernameChanged(value) },
onContinueClick = { onboardingViewModel.saveUsername() }
onProfileImageSelected = { uri ->
onboardingViewModel.onProfileImageSelected(uri)
},
onProfileImageDeleted = {
onboardingViewModel.markProfileImageDeleted()
},
onContinueClick = { onboardingViewModel.saveProfile() }
)
}

composable(HangshaDestinations.OnboardingWelcome.route) {
OnboardingWelcomeScreen(
onMyPageClick = {
navController.navigate(BottomTab.MyPage.route) {
popUpTo(HangshaDestinations.OnboardingWelcome.route) {
inclusive = true
}
launchSingleTop = true
}
},
onCalendarClick = {
navController.navigate(HangshaDestinations.Main.route) {
popUpTo(HangshaDestinations.OnboardingWelcome.route) {
inclusive = true
}
launchSingleTop = true
}
}
)
}
}
Expand Down Expand Up @@ -531,20 +570,41 @@ fun NavGraphBuilder.mainGraph(navController: NavHostController) {
onRetryClick = { myMemosViewModel.loadMemos() }
)
}
composable(HangshaDestinations.InterestPriority.route) {
composable(
route = HangshaDestinations.InterestPriority.route,
arguments = listOf(
navArgument(HangshaDestinations.InterestPriority.sourceArg) {
type = NavType.StringType
defaultValue = HangshaDestinations.InterestPriority.sourceMyPage
}
)
) { backStackEntry ->
val interestPriorityViewModel: InterestPriorityViewModel = hiltViewModel()
val interestPriorityUiState by interestPriorityViewModel.uiState.collectAsState()
val source = backStackEntry.arguments
?.getString(HangshaDestinations.InterestPriority.sourceArg)
?: HangshaDestinations.InterestPriority.sourceMyPage
val isOnboardingFlow =
source == HangshaDestinations.InterestPriority.sourceOnboarding

LaunchedEffect(interestPriorityUiState.isSaveSuccessful) {
LaunchedEffect(interestPriorityUiState.isSaveSuccessful, isOnboardingFlow) {
if (!interestPriorityUiState.isSaveSuccessful) {
return@LaunchedEffect
}

navController.previousBackStackEntry?.savedStateHandle?.set(
InterestPriorityNavigationKeys.updatedKey,
true
)
navController.popBackStack()
if (isOnboardingFlow) {
navController.navigate(HangshaDestinations.OnboardingWelcome.route) {
popUpTo(HangshaDestinations.Onboarding.route) {
inclusive = true
}
}
} else {
navController.previousBackStackEntry?.savedStateHandle?.set(
InterestPriorityNavigationKeys.updatedKey,
true
)
navController.popBackStack()
}
interestPriorityViewModel.onSaveSuccessConsumed()
}

Expand Down Expand Up @@ -627,7 +687,7 @@ fun NavGraphBuilder.mainGraph(navController: NavHostController) {
},
onSaveProfileEdit = { myPageViewModel.saveProfileEdit() },
onInterestPriorityClick = {
navController.navigate(HangshaDestinations.InterestPriority.route)
navController.navigate(HangshaDestinations.InterestPriority.createRoute())
},
onTimetableClick = {
navController.navigate(BottomTab.Timetable.route) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.example.hangsha_android.ui.view.onboarding

import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
Expand All @@ -15,23 +19,35 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.PhotoCamera
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.graphics.SolidColor
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.example.hangsha_android.R

private val OnboardingContentWidth = 280.dp
private val OnboardingAvatarContainerSize = 190.dp
private val OnboardingAvatarImageSize = 184.dp
private val OnboardingDeleteButtonSize = 42.dp
private val OnboardingCameraButtonSize = 46.dp
private val OnboardingFieldHeight = 37.dp
private val OnboardingButtonHeight = 37.dp
private val OnboardingRoundShape = CircleShape
Expand All @@ -40,13 +56,26 @@ private val OnboardingWhite = Color(0xFFFFFFFF)
private val OnboardingBorder = Color(0xFFE0E0E0)
private val OnboardingPlaceholder = Color(0xFF8F8F8F)
private val OnboardingError = Color(0xFFFF4058)
private val OnboardingAvatarBackground = Color(0xFFD9D9D9)
private val OnboardingCameraIcon = Color(0xFF8F8F8F)

@Composable
fun OnboardingScreen(
uiState: OnboardingUiState,
onUsernameChanged: (String) -> Unit,
onProfileImageSelected: (Uri) -> Unit,
onProfileImageDeleted: () -> Unit,
onContinueClick: () -> Unit
) {
val profileImagePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri ->
if (uri != null) {
onProfileImageSelected(uri)
}
}
val profileImageModel = uiState.profileImageUri ?: uiState.profileImageUrl

Box(
modifier = Modifier
.fillMaxSize()
Expand All @@ -60,26 +89,32 @@ fun OnboardingScreen(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "사용자명 설정",
text = "프로필 설정",
color = OnboardingBlack,
fontSize = 23.sp,
fontWeight = FontWeight.Bold,
letterSpacing = 0.sp
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "행샤에서 사용할 이름을 입력해주세요",
text = "프로필 사진과 이름을 설정해주세요",
color = OnboardingBlack,
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
letterSpacing = 0.sp,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(46.dp))
OnboardingProfileImage(
profileImageModel = profileImageModel,
onPickImageClick = { profileImagePicker.launch("image/*") },
onDeleteImageClick = onProfileImageDeleted
)
Spacer(modifier = Modifier.height(18.dp))
OnboardingTextField(
value = uiState.username,
onValueChange = onUsernameChanged,
placeholder = "사용자명",
placeholder = "닉네임",
isError = uiState.usernameErrorMessage != null
)
uiState.usernameErrorMessage?.let { message ->
Expand All @@ -97,7 +132,73 @@ fun OnboardingScreen(
OnboardingSubmitButton(
onClick = onContinueClick,
enabled = uiState.isSubmitEnabled,
isLoading = uiState.isSavingUsername
isLoading = uiState.isSavingProfile
)
}
}
}

@Composable
private fun OnboardingProfileImage(
profileImageModel: Any?,
onPickImageClick: () -> Unit,
onDeleteImageClick: () -> Unit
) {
val hasProfileImage = profileImageModel.hasRealProfileImage()
val imageModel = if (hasProfileImage) profileImageModel else R.drawable.profile_null_img
val canDeleteProfileImage = profileImageModel.isUserProfileImage()

Box(modifier = Modifier.size(OnboardingAvatarContainerSize)) {
Box(
modifier = Modifier
.align(Alignment.Center)
.size(OnboardingAvatarImageSize)
.clip(CircleShape)
.background(OnboardingAvatarBackground),
contentAlignment = Alignment.Center
) {
AsyncImage(
model = imageModel,
contentDescription = "프로필 이미지",
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}

if (canDeleteProfileImage) {
Box(
modifier = Modifier
.align(Alignment.TopStart)
.size(OnboardingDeleteButtonSize)
.clip(CircleShape)
.background(OnboardingError)
.clickable(onClick = onDeleteImageClick),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = "프로필 이미지 삭제",
tint = OnboardingWhite,
modifier = Modifier.size(30.dp)
)
}
}

Box(
modifier = Modifier
.align(Alignment.BottomEnd)
.size(OnboardingCameraButtonSize)
.clip(CircleShape)
.background(OnboardingWhite)
.border(2.dp, OnboardingBorder, CircleShape)
.clickable(onClick = onPickImageClick),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Rounded.PhotoCamera,
contentDescription = "프로필 이미지 선택",
tint = OnboardingCameraIcon,
modifier = Modifier.size(28.dp)
)
}
}
Expand Down Expand Up @@ -174,7 +275,7 @@ private fun OnboardingSubmitButton(
OnboardingProgressIndicator(size = 16.dp)
} else {
Text(
text = "계속",
text = "프로필 설정하기",
color = OnboardingWhite,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
Expand All @@ -193,3 +294,34 @@ private fun OnboardingProgressIndicator(size: Dp) {
strokeWidth = 2.dp
)
}

private fun Any?.hasRealProfileImage(): Boolean {
return when (this) {
is Uri -> true
is String -> isRealProfileImageUrl()
else -> false
}
}

private fun Any?.isUserProfileImage(): Boolean {
return when (this) {
is Uri -> true
is String -> isRealProfileImageUrl() && !isDefaultProfileImageUrl()
else -> false
}
}

private fun String?.isRealProfileImageUrl(): Boolean {
val normalized = this?.trim().orEmpty()
return normalized.isNotBlank() &&
!normalized.equals("null", ignoreCase = true) &&
(normalized.startsWith("http://") ||
normalized.startsWith("https://") ||
normalized.startsWith("content://") ||
normalized.startsWith("file://"))
}

private fun String?.isDefaultProfileImageUrl(): Boolean {
val normalized = this?.trim().orEmpty()
return normalized.contains("/default/43513b43-2f84-4f0f-8de8-7d61120fe3aa.png")
}
Loading
Loading