Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 프로필 설정 UI 구현 #103

Merged
merged 9 commits into from
Feb 8, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.kusitms.presentation.model.profile.edit

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.kusitms.presentation.common.ui.KusitmsTabItem
import com.kusitms.presentation.common.ui.KusitmsTabRow
import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette
import com.kusitms.presentation.model.signIn.InterestItem
import com.kusitms.presentation.model.signIn.PartCategory
import com.kusitms.presentation.model.signIn.mapCategoryToValue
import com.kusitms.presentation.ui.profile.edit.LikeBottomSheetContent
import com.kusitms.presentation.ui.profile.edit.PartSelectItem
import com.kusitms.presentation.ui.signIn.component.LikeCategoryItem
import kotlinx.coroutines.ExperimentalCoroutinesApi


@Composable
fun PartSelectColumn(viewModel: ProfileEditViewModel) {
val filteredCategories = com.kusitms.presentation.model.signIn.categories.filter { it.name != "기타" }
LazyColumn(
modifier = Modifier
.padding(horizontal = 24.dp)
) {
items(filteredCategories) { category ->
PartSelectItem(
category = category,
onClick = { selectedCategory ->
val selectedValue = mapCategoryToValue(selectedCategory.name)
viewModel.updateSelectedPart(selectedValue)
},
viewModel = viewModel
)
}
}
}



@OptIn(ExperimentalMaterial3Api::class)
@ExperimentalMaterialApi
@Composable
fun LikeCategoryBottomSheet(
viewModel: ProfileEditViewModel,
openBottomSheet: Boolean = false,
onChangeOpenBottomSheet: (Boolean) -> Unit = {},
) {
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)

if (openBottomSheet) {
ModalBottomSheet(
containerColor = KusitmsColorPalette.current.Grey600,
dragHandle = { Box(Modifier.height(0.dp)) },
onDismissRequest = { onChangeOpenBottomSheet(false) },
sheetState = bottomSheetState,
modifier = Modifier
.fillMaxWidth()
.height(704.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.systemBarsPadding()
.statusBarsPadding()
) {
LikeBottomSheetContent(
viewModel = viewModel,
onClick = { onChangeOpenBottomSheet(false) })
}

}
}
}




@Composable
fun LikeCategoryTab(selectedCategory: PartCategory, onCategorySelected: (PartCategory) -> Unit, viewModel: ProfileEditViewModel) {
KusitmsTabRow(
tabItemList = com.kusitms.presentation.model.signIn.categories,
tabContent = { category ->
KusitmsTabItem(
text = category.name,
isSelected = category == selectedCategory,
onSelect = { onCategorySelected(category) }
)
}
)
when (selectedCategory.name) {
"기획" -> LikeCategoryItems(category = com.kusitms.presentation.model.signIn.categories[0], viewModel = viewModel)
"개발" -> LikeCategoryItems(category = com.kusitms.presentation.model.signIn.categories[1], viewModel = viewModel)
"디자인" -> LikeCategoryItems(category = com.kusitms.presentation.model.signIn.categories[2], viewModel = viewModel)
"기타" -> LikeCategoryItems(category = com.kusitms.presentation.model.signIn.categories[3], viewModel = viewModel)
else -> {}
}
}


@OptIn(ExperimentalCoroutinesApi::class)
@Composable
fun LikeCategoryItems(category: PartCategory, viewModel: ProfileEditViewModel) {
val selectedInterests = viewModel.interests.collectAsState().value.toMutableSet()

LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 16.dp)
.height(436.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(category.subCategories) { subCategory ->
val interestItem = InterestItem(mapCategoryToValue(category.name), subCategory)
LikeCategoryItem(
subCategoryName = subCategory,
isSelected = interestItem in selectedInterests,
onSelect = {
if (interestItem in selectedInterests) {
selectedInterests.remove(interestItem)
} else {
selectedInterests.add(interestItem)
}
viewModel.updateInterests(selectedInterests.toList())
}
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.kusitms.presentation.model.profile.edit

data class ProfileEditUiState(
val currentSelectedProfileFilter: String = "기본 프로필",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package com.kusitms.presentation.model.profile.edit

import android.net.Uri
import androidx.lifecycle.ViewModel
import com.kusitms.presentation.model.signIn.InterestItem
import com.kusitms.presentation.model.signIn.LinkItem
import com.kusitms.presentation.model.signIn.LinkType
import com.kusitms.presentation.model.signIn.SignInStatus
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject

@HiltViewModel
class ProfileEditViewModel @Inject constructor(

) : ViewModel() {
private val _uiState = MutableStateFlow(ProfileEditUiState())
val uiState: StateFlow<ProfileEditUiState> = _uiState.asStateFlow()

private val _isAllFieldsValid = MutableStateFlow(false)
val isAllFieldsValid: StateFlow<Boolean> = _isAllFieldsValid

private val _expanded = MutableStateFlow(false)
val expended: StateFlow<Boolean> = _expanded.asStateFlow()

private val _favoriteCategory = MutableStateFlow<List<String>?>(null)
val favoriteCategory: StateFlow<List<String>?> = _favoriteCategory

private val _interests = MutableStateFlow<List<InterestItem>>(emptyList())
val interests: StateFlow<List<InterestItem>> = _interests.asStateFlow()

private val _name = MutableStateFlow("")
val name: StateFlow<String> = _name

private val _major = MutableStateFlow("")
val major: StateFlow<String> = _major

private val _phoneNum = MutableStateFlow("")
val phoneNum: StateFlow<String> = _phoneNum

private val _email = MutableStateFlow("")
val email: StateFlow<String> = _email

private val _emailError = MutableStateFlow<String?>(null)
val emailError: StateFlow<String?> = _emailError

private val _phoneNumError = MutableStateFlow<String?>(null)
val phoneNumError: StateFlow<String?> = _phoneNumError


private val _selectedImage = MutableStateFlow<Uri?>(null)
val selectedImage: StateFlow<Uri?> = _selectedImage.asStateFlow()

private val _linkItems = MutableStateFlow<List<LinkItem>>(listOf(LinkItem(LinkType.LINK, "")))
val linkItems: StateFlow<List<LinkItem>> = _linkItems.asStateFlow()

private val _introduce = MutableStateFlow("")
val introduce: StateFlow<String> = _introduce

private val _signInStatus = MutableStateFlow(SignInStatus.DEFAULT)
val signInStatus : StateFlow<SignInStatus> = _signInStatus

private val _selectedPart = MutableStateFlow<String?>(null)
val selectedPart: StateFlow<String?> = _selectedPart

fun changeSelectProfileFilter(category: String) {
_uiState.value = _uiState.value.copy(currentSelectedProfileFilter = category)
_expanded.value = false
}

fun toggleExpanded() {
_expanded.value = !_expanded.value
}

fun updateMajor(newMajor: String) {
_major.value = newMajor
validateFields()
}


fun updateSelectedPart(part: String) {
_selectedPart.value = part
validateFields()
}

fun updateInterests(interestItems: List<InterestItem>) {
_interests.value = interestItems
validateFields()
}

fun updateEmail(newEmail: String) {
_email.value = newEmail
if (!isValidEmail(newEmail)) {
_emailError.value = "유효한 이메일 주소를 입력해주세요"
} else {
_emailError.value = null
}
validateFields()
}

fun updatePhoneNumber(newPhoneNumber: String) {
_phoneNum.value = newPhoneNumber
if (!isValidPhoneNumber(newPhoneNumber)) {
_phoneNumError.value = "유효한 전화번호를 입력해주세요 (010-1234-1234)"
} else {
_phoneNumError.value = null
}
validateFields()
}

fun updateSelectedImage(uri: Uri?) {
_selectedImage.value = uri
}

fun updateLinkTypeAt(index: Int, linkType: LinkType) {
val updatedItems = _linkItems.value.toMutableList()
if (index in updatedItems.indices) {
val currentItem = updatedItems[index]
updatedItems[index] = currentItem.copy(linkType = linkType)
_linkItems.value = updatedItems
}
}



fun addLinkItem() {
val newLinkItem = LinkItem(LinkType.LINK, "") //기본 설정값
_linkItems.value = _linkItems.value + newLinkItem
}

fun updateLinkItem(index: Int, linkType: LinkType, url: String) {
val updatedItems = _linkItems.value.toMutableList()
if (index in updatedItems.indices) {
updatedItems[index] = LinkItem(linkType, url)
_linkItems.value = updatedItems
}
}

fun removeLinkItem() {
if (_linkItems.value.isNotEmpty()) {
_linkItems.value = _linkItems.value.dropLast(1)
}
}

fun updateIntroduce(introduce: String) {
_introduce.value = introduce
}


private fun validateFields() {
_isAllFieldsValid.value = _major.value.isNotBlank() &&
_selectedPart.value != null &&
_interests.value.isNotEmpty() &&
_email.value.isNotBlank() &&
_phoneNum.value.isNotBlank()
}

private fun isValidEmail(email: String): Boolean {
val emailRegex = Regex("^\\w+@[a-zA-Z_]+?\\.[a-zA-Z]{2,3}\$")
return email.matches(emailRegex)
}

private fun isValidPhoneNumber(phoneNumber: String): Boolean {
val phoneRegex = Regex("^01(?:0|1|[6-9])-(?:\\d{3}|\\d{4})-\\d{4}\$")
return phoneNumber.matches(phoneRegex)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.kusitms.presentation.model.profile.edit

data class ProfileFilterList(
val id: Int,
val name: String,
)

val categories = listOf(
ProfileFilterList(1, name = "기본 프로필"),
ProfileFilterList(2, name = "추가 프로필"),
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.kusitms.presentation.navigation

import ProfileDetailScreen
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
Expand Down Expand Up @@ -62,6 +61,7 @@ import com.kusitms.presentation.ui.notice.NoticeScreen
import com.kusitms.presentation.ui.notice.detail.NoticeDetailScreen
import com.kusitms.presentation.ui.notice.search.NoticeSearchScreen
import com.kusitms.presentation.ui.profile.ProfileScreen
import com.kusitms.presentation.ui.profile.edit.ProfileEditScreen
import com.kusitms.presentation.ui.profile.search.ProfileSearchScreen
import com.kusitms.presentation.ui.setting.SettingMember
import com.kusitms.presentation.ui.setting.SettingNonMember
Expand Down Expand Up @@ -271,7 +271,10 @@ fun MainNavigation() {
arguments = NavRoutes.MyProfileDetail.navArguments
) {
MyProfileScreen(
onBack = { navController.navigateUp() }
onBack = { navController.navigateUp() },
onClickModify = {
navController.navigate(NavRoutes.ProfileEdit.route)
}
)
}

Expand Down Expand Up @@ -362,7 +365,10 @@ fun MainNavigation() {
arguments = NavRoutes.ProfileDetail.navArguments
) {
ProfileDetailScreen(
onBack = { navController.navigateUp() }
onBack = { navController.navigateUp() },
onClickModify = {
navController.navigate(NavRoutes.ProfileEdit.route)
}
)
}

Expand All @@ -381,6 +387,11 @@ fun MainNavigation() {
)
}

kusitmsComposableWithAnimation(NavRoutes.ProfileEdit.route) {
ProfileEditScreen(
onBack = { navController.navigateUp() }
)
}
Comment on lines +390 to +394
Copy link
Owner

Choose a reason for hiding this comment

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

제가 이전 코드를 짠 것을 사용하는데에 문제가 있었는지 혹시 궁금합니다 🥹 .... 변경 사항 보고 놀래서 들어왔는데 재사용을 안하시고 새로 만드셨길래

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

민서님 코드 재사용 하려고 했는데, Composable 선언할 때 파라미터로 SignInViewModel을 받으셨더라구요!
그래서 저도 SignInViewModel을 써야하거나, 뷰를 새로 만들던가 해야했는데
민서님 코드와 달리 프로필 설정 부분에서 이메일/전화번호 수정이 가능하기도 했고
Profile 설정쪽 뷰모델을 아예 다르게 지정하는게 맞을 것 같아서 뷰를 새로 만들고 ProfileEditViewModel을 파라미터로 설정했습니당
ProfileEditViewModel에서 사용하는 API, SignInViewModel에서 사용하는 API가 달라 분리를 하긴 했습니다.!

이 부분에 대해서는 저도 얘기를 드리고 싶었는데, 차라리 ViewModel을 하나로 통일하고 재사용하는 식으로 리팩토링 하는게 나을 것 같아요! 피그마상에서도 자잘하게 다른 부분이 꽤나 있어 추후 작업이 필요해 보입니당.. 그래도 ViewModel이 없는 쪽에서는 최대한 민서님 코드를 재사용 해둔 것 같아요 이 부분에 대해서 저희끼리 회의할 때 얘기하면 좋을 것 같습니다!

Copy link
Owner

Choose a reason for hiding this comment

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

같은 뷰모델을 사용해도 될것이라 생각했는데 이전 화면에는 필요없는 api도 같이 딸려 들어있다보니 조금 낭비적인 측면도 있긴 하겠네요 .. 🤔
추후에 아린님 말씀처럼 공통 뷰 모델로 통일하는 방안이 좋은 해결 방안일것 같습니다 출시 후에 논의해봐요
답변 감사합니다!



kusitmsComposableWithAnimation(NavRoutes.ImageViewer.route) {
Expand Down
Loading