diff --git a/common/resource/src/main/res/drawable/ic_setting.png b/common/resource/src/main/res/drawable/ic_setting.png new file mode 100644 index 00000000..fd5069cc Binary files /dev/null and b/common/resource/src/main/res/drawable/ic_setting.png differ diff --git a/common/resource/src/main/res/drawable/img_empty_profile.png b/common/resource/src/main/res/drawable/img_empty_profile.png index 790c22a8..ab49db7f 100644 Binary files a/common/resource/src/main/res/drawable/img_empty_profile.png and b/common/resource/src/main/res/drawable/img_empty_profile.png differ diff --git a/core/designsystem/src/main/java/com/idiotfrogs/designsystem/component/button/MSButton.kt b/core/designsystem/src/main/java/com/idiotfrogs/designsystem/component/button/MSButton.kt index 77293ba2..0d05c867 100644 --- a/core/designsystem/src/main/java/com/idiotfrogs/designsystem/component/button/MSButton.kt +++ b/core/designsystem/src/main/java/com/idiotfrogs/designsystem/component/button/MSButton.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.idiotfrogs.designsystem.component.MSText import com.idiotfrogs.designsystem.theme.MSTheme @@ -33,6 +34,7 @@ fun MSButton( modifier: Modifier = Modifier, enabled: Boolean = true, isRounded: Boolean = true, + cornerRadius: Dp = 12.dp, colors: ButtonColors = ButtonDefaults.buttonColors( containerColor = MSTheme.color.primaryNormal, disabledContainerColor = MSTheme.color.primaryLight @@ -51,7 +53,7 @@ fun MSButton( ) { val isPressed by interactionSource.collectIsPressedAsState() val cornerRadius by animateDpAsState( - targetValue = if (isRounded) 12.dp else 0.dp, + targetValue = if (isRounded) cornerRadius else 0.dp, animationSpec = tween(durationMillis = 300), label = "cornerRadiusAnimation", ) diff --git a/core/designsystem/stability/designsystem-debug.stability b/core/designsystem/stability/designsystem-debug.stability index 7a26d09b..534b2eae 100644 --- a/core/designsystem/stability/designsystem-debug.stability +++ b/core/designsystem/stability/designsystem-debug.stability @@ -315,13 +315,14 @@ public fun com.idiotfrogs.designsystem.component.TwoByTwo(images: kotlin.collect - images: RUNTIME (requires runtime check) @Composable -public fun com.idiotfrogs.designsystem.component.button.MSButton(modifier: androidx.compose.ui.Modifier, enabled: kotlin.Boolean, isRounded: kotlin.Boolean, colors: androidx.compose.material3.ButtonColors, pressColors: androidx.compose.material3.ButtonColors, elevation: androidx.compose.material3.ButtonElevation?, border: androidx.compose.foundation.BorderStroke?, wavyStrokeColor: androidx.compose.ui.graphics.Color?, contentPadding: androidx.compose.foundation.layout.PaddingValues, interactionSource: androidx.compose.foundation.interaction.MutableInteractionSource, onClick: kotlin.Function0, content: @[Composable] @[ExtensionFunctionType] androidx.compose.runtime.internal.ComposableFunction1): kotlin.Unit +public fun com.idiotfrogs.designsystem.component.button.MSButton(modifier: androidx.compose.ui.Modifier, enabled: kotlin.Boolean, isRounded: kotlin.Boolean, cornerRadius: androidx.compose.ui.unit.Dp, colors: androidx.compose.material3.ButtonColors, pressColors: androidx.compose.material3.ButtonColors, elevation: androidx.compose.material3.ButtonElevation?, border: androidx.compose.foundation.BorderStroke?, wavyStrokeColor: androidx.compose.ui.graphics.Color?, contentPadding: androidx.compose.foundation.layout.PaddingValues, interactionSource: androidx.compose.foundation.interaction.MutableInteractionSource, onClick: kotlin.Function0, content: @[Composable] @[ExtensionFunctionType] androidx.compose.runtime.internal.ComposableFunction1): kotlin.Unit skippable: true restartable: true params: - modifier: STABLE (marked @Stable or @Immutable) - enabled: STABLE (primitive type) - isRounded: STABLE (primitive type) + - cornerRadius: STABLE (marked @Stable or @Immutable) - colors: STABLE (marked @Stable or @Immutable) - pressColors: STABLE (marked @Stable or @Immutable) - elevation: STABLE (marked @Stable or @Immutable) diff --git a/core/designsystem/stability/designsystem-release.stability b/core/designsystem/stability/designsystem-release.stability index 7a26d09b..534b2eae 100644 --- a/core/designsystem/stability/designsystem-release.stability +++ b/core/designsystem/stability/designsystem-release.stability @@ -315,13 +315,14 @@ public fun com.idiotfrogs.designsystem.component.TwoByTwo(images: kotlin.collect - images: RUNTIME (requires runtime check) @Composable -public fun com.idiotfrogs.designsystem.component.button.MSButton(modifier: androidx.compose.ui.Modifier, enabled: kotlin.Boolean, isRounded: kotlin.Boolean, colors: androidx.compose.material3.ButtonColors, pressColors: androidx.compose.material3.ButtonColors, elevation: androidx.compose.material3.ButtonElevation?, border: androidx.compose.foundation.BorderStroke?, wavyStrokeColor: androidx.compose.ui.graphics.Color?, contentPadding: androidx.compose.foundation.layout.PaddingValues, interactionSource: androidx.compose.foundation.interaction.MutableInteractionSource, onClick: kotlin.Function0, content: @[Composable] @[ExtensionFunctionType] androidx.compose.runtime.internal.ComposableFunction1): kotlin.Unit +public fun com.idiotfrogs.designsystem.component.button.MSButton(modifier: androidx.compose.ui.Modifier, enabled: kotlin.Boolean, isRounded: kotlin.Boolean, cornerRadius: androidx.compose.ui.unit.Dp, colors: androidx.compose.material3.ButtonColors, pressColors: androidx.compose.material3.ButtonColors, elevation: androidx.compose.material3.ButtonElevation?, border: androidx.compose.foundation.BorderStroke?, wavyStrokeColor: androidx.compose.ui.graphics.Color?, contentPadding: androidx.compose.foundation.layout.PaddingValues, interactionSource: androidx.compose.foundation.interaction.MutableInteractionSource, onClick: kotlin.Function0, content: @[Composable] @[ExtensionFunctionType] androidx.compose.runtime.internal.ComposableFunction1): kotlin.Unit skippable: true restartable: true params: - modifier: STABLE (marked @Stable or @Immutable) - enabled: STABLE (primitive type) - isRounded: STABLE (primitive type) + - cornerRadius: STABLE (marked @Stable or @Immutable) - colors: STABLE (marked @Stable or @Immutable) - pressColors: STABLE (marked @Stable or @Immutable) - elevation: STABLE (marked @Stable or @Immutable) diff --git a/core/network/src/main/java/com/idiotfrogs/network/service/UserService.kt b/core/network/src/main/java/com/idiotfrogs/network/service/UserService.kt index b1b890d1..2681a36e 100644 --- a/core/network/src/main/java/com/idiotfrogs/network/service/UserService.kt +++ b/core/network/src/main/java/com/idiotfrogs/network/service/UserService.kt @@ -24,8 +24,8 @@ interface UserService { @Multipart suspend fun updateMyProfile( @Path("userId") userId: Long, - @Part("profileImage") profileImage: MultipartBody.Part, - @Part("userUpdateDto") userUpdateRequest: UserUpdateRequest + @Part profileImage: MultipartBody.Part, + @Query("nickname") nickname: String ): UserResponse @PATCH("users/sign-up") diff --git a/data/src/main/java/com/idiotfrogs/data/datasource/user/UserDataSource.kt b/data/src/main/java/com/idiotfrogs/data/datasource/user/UserDataSource.kt index 0756b9b6..3a842479 100644 --- a/data/src/main/java/com/idiotfrogs/data/datasource/user/UserDataSource.kt +++ b/data/src/main/java/com/idiotfrogs/data/datasource/user/UserDataSource.kt @@ -13,7 +13,7 @@ interface UserDataSource { suspend fun updateMyProfile( userId: Long, profileImage: MultipartBody.Part, - userUpdateRequest: UserUpdateRequest + nickname: String, ): UserResponse suspend fun signUp( diff --git a/data/src/main/java/com/idiotfrogs/data/datasource/user/UserDataSourceImpl.kt b/data/src/main/java/com/idiotfrogs/data/datasource/user/UserDataSourceImpl.kt index 116e9db9..9cf95bac 100644 --- a/data/src/main/java/com/idiotfrogs/data/datasource/user/UserDataSourceImpl.kt +++ b/data/src/main/java/com/idiotfrogs/data/datasource/user/UserDataSourceImpl.kt @@ -21,12 +21,12 @@ class UserDataSourceImpl @Inject constructor( override suspend fun updateMyProfile( userId: Long, profileImage: MultipartBody.Part, - userUpdateRequest: UserUpdateRequest + nickname: String, ): UserResponse { return userService.updateMyProfile( userId = userId, profileImage = profileImage, - userUpdateRequest = userUpdateRequest + nickname = nickname ) } diff --git a/data/src/main/java/com/idiotfrogs/data/repository/user/UserRepository.kt b/data/src/main/java/com/idiotfrogs/data/repository/user/UserRepository.kt index a9e18239..0cd9c322 100644 --- a/data/src/main/java/com/idiotfrogs/data/repository/user/UserRepository.kt +++ b/data/src/main/java/com/idiotfrogs/data/repository/user/UserRepository.kt @@ -12,8 +12,8 @@ interface UserRepository { suspend fun updateMyProfile( userId: Long, - profileImage: File, - userUpdateRequest: UserUpdateRequest + profileImage: File?, + nickname: String ): UserResponse suspend fun signUp( diff --git a/data/src/main/java/com/idiotfrogs/data/repository/user/UserRepositoryImpl.kt b/data/src/main/java/com/idiotfrogs/data/repository/user/UserRepositoryImpl.kt index c14025e6..c832dbc8 100644 --- a/data/src/main/java/com/idiotfrogs/data/repository/user/UserRepositoryImpl.kt +++ b/data/src/main/java/com/idiotfrogs/data/repository/user/UserRepositoryImpl.kt @@ -3,10 +3,10 @@ package com.idiotfrogs.data.repository.user import com.idiotfrogs.data.datasource.user.UserDataSource import com.idiotfrogs.model.user.ProfileResponse import com.idiotfrogs.model.user.UserResponse -import com.idiotfrogs.model.user.UserUpdateRequest import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody import java.io.File import javax.inject.Inject @@ -23,15 +23,21 @@ class UserRepositoryImpl @Inject constructor( override suspend fun updateMyProfile( userId: Long, - profileImage: File, - userUpdateRequest: UserUpdateRequest + profileImage: File?, + nickname: String, ): UserResponse { - val imageRequestBody = profileImage.asRequestBody("image/jpeg".toMediaType()) - val imagePart = MultipartBody.Part.createFormData("profileImage", profileImage.name, imageRequestBody) + val imageRequestBody = profileImage?.asRequestBody("image/jpeg".toMediaType()) + ?: "".toRequestBody("image/*".toMediaType()) + val imagePart = MultipartBody.Part.createFormData( + "profileImage", + profileImage?.name ?: "profileImage", // 기본 이미지 대응 + imageRequestBody + ) + return userDataSource.updateMyProfile( userId = userId, profileImage = imagePart, - userUpdateRequest = userUpdateRequest + nickname = nickname ) } diff --git a/domain/src/main/java/com/idiotfrogs/domain/usecase/user/UpdateMyProfileUseCase.kt b/domain/src/main/java/com/idiotfrogs/domain/usecase/user/UpdateMyProfileUseCase.kt index cac343d4..af721fdd 100644 --- a/domain/src/main/java/com/idiotfrogs/domain/usecase/user/UpdateMyProfileUseCase.kt +++ b/domain/src/main/java/com/idiotfrogs/domain/usecase/user/UpdateMyProfileUseCase.kt @@ -2,7 +2,6 @@ package com.idiotfrogs.domain.usecase.user import com.idiotfrogs.data.repository.user.UserRepository import com.idiotfrogs.model.user.UserResponse -import com.idiotfrogs.model.user.UserUpdateRequest import com.idiotfrogs.util.safeCatching import java.io.File import javax.inject.Inject @@ -12,13 +11,13 @@ class UpdateMyProfileUseCase @Inject constructor( ) { suspend operator fun invoke( userId: Long, - profileImage: File, - userUpdateRequest: UserUpdateRequest + profileImage: File?, + nickname: String, ): Result = safeCatching { userRepository.updateMyProfile( userId, profileImage, - userUpdateRequest + nickname ) } } \ No newline at end of file diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/component/EditProfileHeader.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/component/EditProfileHeader.kt index ced46455..05e89138 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/component/EditProfileHeader.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/component/EditProfileHeader.kt @@ -1,10 +1,12 @@ package com.idiotfrogs.profile.component import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -12,11 +14,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.idiotfrogs.designsystem.component.MSText +import com.idiotfrogs.designsystem.component.button.MSButton import com.idiotfrogs.designsystem.theme.MSTheme import com.idiotfrogs.designsystem.util.noRippleClickable import com.idiotfrogs.resource.R @@ -28,33 +32,42 @@ fun ProfileHeader( onBack: () -> Unit, onSave: () -> Unit, ) { - Row( + Box( modifier = modifier .fillMaxWidth() - .padding(vertical = 16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + .height(56.dp), ) { Image( - modifier = Modifier.noRippleClickable(onBack), + modifier = Modifier + .align(Alignment.CenterStart) + .size(24.dp) + .noRippleClickable(onBack), painter = painterResource(R.drawable.ic_chevron_left), contentDescription = "chevron left" ) MSText( - text = "프로필", + modifier = Modifier.align(Alignment.Center), + text = "프로필 수정", fontWeight = FontWeight.Bold, - fontSize = 14.dp, + fontSize = 20.dp, color = MSTheme.color.black ) - MSText( - modifier = Modifier.noRippleClickable { - if (isChanged) onSave() - }, - text = "저장", - fontWeight = FontWeight.Bold, - fontSize = 14.dp, - color = if (isChanged) MSTheme.color.primaryNormal else MSTheme.color.greyG2, - ) + MSButton( + modifier = Modifier + .align(Alignment.CenterEnd) + .height(32.dp), + cornerRadius = 8.dp, + contentPadding = PaddingValues(horizontal = 1.dp), + enabled = isChanged, + onClick = onSave + ) { + MSText( + text = "저장", + color = if (isChanged) MSTheme.color.primaryDark else Color(0xFF84B591), + fontSize = 14.dp, + fontWeight = FontWeight.Bold + ) + } } } diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileCard.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileCard.kt index 7e29bce7..4eeefd6c 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileCard.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileCard.kt @@ -2,13 +2,18 @@ package com.idiotfrogs.profile.component import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -19,6 +24,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.idiotfrogs.designsystem.component.MSText +import com.idiotfrogs.designsystem.component.button.MSButton import com.idiotfrogs.designsystem.theme.MSTheme import com.idiotfrogs.designsystem.util.noRippleClickable import com.idiotfrogs.resource.R @@ -31,63 +37,51 @@ fun ProfileCard( nickname: String, onEditClick: () -> Unit, ) { - Row( - modifier = modifier - .background( - color = MSTheme.color.white, - shape = RoundedCornerShape(10.dp) - ) - .padding(horizontal = 16.dp, vertical = 20.dp), - verticalAlignment = Alignment.CenterVertically + Column( + modifier = modifier.background(color = MSTheme.color.white), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { imageUrl?.let { GlideImage( modifier = Modifier - .size(54.dp) + .size(80.dp) .clip(CircleShape), imageModel = { it } ) } ?: run { Image( - modifier = Modifier.size(54.dp), - painter = painterResource(R.drawable.img_profile_54), + modifier = Modifier.size(width = 78.dp, height = 80.dp), + painter = painterResource(R.drawable.img_empty_profile), contentDescription = "empty_profile" ) } - - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.height(8.dp)) MSText( text = nickname, fontWeight = FontWeight.Bold, - fontSize = 16.dp, + fontSize = 24.dp, color = MSTheme.color.greyG5 ) - Spacer(modifier = Modifier.weight(1f)) - Row( - modifier = Modifier - .background( - color = MSTheme.color.greyG1, - shape = RoundedCornerShape(29.dp) - ) - .padding( - top = 8.dp, bottom = 8.dp, - start = 8.dp, end = 10.dp - ) - .noRippleClickable(onClick = onEditClick), - verticalAlignment = Alignment.CenterVertically + Spacer(modifier = Modifier.height(20.dp)) + MSButton( + modifier = Modifier.height(32.dp), + contentPadding = PaddingValues(horizontal = 8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MSTheme.color.greyG5, + disabledContainerColor = MSTheme.color.greyG5 + ), + pressColors = ButtonDefaults.buttonColors( + containerColor = MSTheme.color.greyG5, + disabledContainerColor = MSTheme.color.greyG5 + ), + onClick = onEditClick ) { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.ic_edit), - contentDescription = "edit", - tint = MSTheme.color.greyG3 - ) - Spacer(modifier = Modifier.width(2.dp)) MSText( text = "프로필 수정", - fontWeight = FontWeight.Medium, + fontWeight = FontWeight.Bold, fontSize = 12.dp, - color = MSTheme.color.greyG3 + color = MSTheme.color.white ) } } diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileHeader.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileHeader.kt index b8f133a4..8b2c81b2 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileHeader.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileHeader.kt @@ -43,15 +43,15 @@ fun ProfileHeader( MSText( text = "프로필", fontWeight = FontWeight.Bold, - fontSize = 14.dp, + fontSize = 20.dp, color = MSTheme.color.greyG5 ) - MSText( - modifier = Modifier.noRippleClickable(onClick = onSetting), - text = "설정", - fontWeight = FontWeight.Medium, - fontSize = 14.dp, - color = MSTheme.color.greyG3 + Image( + modifier = Modifier + .size(24.dp) + .noRippleClickable(onClick = onSetting), + painter = painterResource(R.drawable.ic_setting), + contentDescription = "설정" ) } } diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileTicketCard.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileTicketCard.kt index 18b0c210..9403c4fb 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileTicketCard.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/component/ProfileTicketCard.kt @@ -1,10 +1,13 @@ package com.idiotfrogs.profile.component import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -16,6 +19,7 @@ import androidx.compose.ui.unit.dp import com.idiotfrogs.designsystem.component.MSText import com.idiotfrogs.designsystem.theme.MSTheme import com.idiotfrogs.designsystem.util.noRippleClickable +import com.idiotfrogs.designsystem.util.wavyStroke import com.skydoves.landscapist.glide.GlideImage @Composable @@ -25,39 +29,51 @@ fun ProfileTicketCard( date: String, onClick: () -> Unit, ) { - Column( - modifier = Modifier - .background( - color = MSTheme.color.white, - shape = RoundedCornerShape(16.dp) - ) - .padding(horizontal = 12.dp, vertical = 16.dp) - .noRippleClickable(onClick = onClick) - ) { - GlideImage( - modifier = Modifier.aspectRatio(1f), - imageModel = { imageUrl }, - ) - Spacer(modifier = Modifier.height(12.dp)) - MSText( - text = title, - fontWeight = FontWeight.Bold, - fontSize = 16.dp, - color = MSTheme.color.greyG5, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Spacer(modifier = Modifier.height(6.dp)) - MSText( - text = date, - fontWeight = FontWeight.Normal, - fontSize = 12.dp, - color = MSTheme.color.greyG3 + Column(modifier = Modifier.noRippleClickable(onClick = onClick)) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(55.dp) + .wavyStroke( + amplitude = 1.dp, + spacing = 2.dp, + color = MSTheme.color.greyG5, + fillColor = MSTheme.color.primaryNormal + ) ) + Column( + modifier = Modifier + .offset(y = (-5).dp) + .fillMaxWidth() + .height(133.dp) + .wavyStroke( + amplitude = 1.dp, + spacing = 2.dp, + color = MSTheme.color.greyG5, + fillColor = MSTheme.color.white + ) + .padding(12.dp) + ) { + MSText( + text = title, + fontWeight = FontWeight.Bold, + fontSize = 16.dp, + color = MSTheme.color.greyG5, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(6.dp)) + MSText( + text = date, + fontWeight = FontWeight.Normal, + fontSize = 14.dp, + color = MSTheme.color.greyG3 + ) + } } } -@Preview +@Preview(showBackground = true) @Composable private fun ProfileTicketCardPreview() { ProfileTicketCard( diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/editprofile/EditProfileScreen.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/editprofile/EditProfileScreen.kt index a02176b2..69146ade 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/editprofile/EditProfileScreen.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/editprofile/EditProfileScreen.kt @@ -1,6 +1,5 @@ package com.idiotfrogs.profile.editprofile -import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -23,6 +22,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -35,7 +35,9 @@ import com.idiotfrogs.designsystem.component.MSTextField import com.idiotfrogs.designsystem.theme.MSTheme import com.idiotfrogs.designsystem.util.noRippleClickable import com.idiotfrogs.designsystem.util.rememberPickerState +import com.idiotfrogs.designsystem.util.wavyStroke import com.idiotfrogs.extension.toFile +import com.idiotfrogs.model.user.ProfileResponse import com.idiotfrogs.navigation.LocalComposeMSNavigator import com.idiotfrogs.profile.component.EditProfileBottomSheet import com.idiotfrogs.profile.component.ProfileHeader @@ -58,37 +60,51 @@ fun EditProfileRoute( } Box(modifier = Modifier.fillMaxSize()) { - EditProfileScreen(onAction = viewModel::onAction) - + uiState.data?.let { data -> + EditProfileScreen( + data = data, + onAction = viewModel::onAction + ) + } MSLoadingOverlay(visible = uiState.isLoading) } } @Composable fun EditProfileScreen( + data: EditProfileData, onAction: (EditProfileAction) -> Unit ) { val context = LocalContext.current var isChanged by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) } + // 기본 이미지 사용 플래그 (이미지 없는 경우와 구분 용도) + var useDefaultImage by remember { mutableStateOf(false) } val pickerState = rememberPickerState() var imageUri by remember(pickerState.first) { mutableStateOf(pickerState.first) } val launchImagePicker = pickerState.second - val textFieldState = rememberTextFieldState() + val user = data.user ?: return // 유저 정보 없을 시 early-return + + val textFieldState = rememberTextFieldState(initialText = user.nickname) - LaunchedEffect(textFieldState.text) { - // TODO: 추후 기존 프로필과 비교 로직 작성 - isChanged = textFieldState.text.isNotEmpty() + + LaunchedEffect(textFieldState.text, imageUri) { + isChanged = user.nickname != textFieldState.text || // 닉네임이 변경 되었거나 + imageUri != null // 이미지가 로드되어 Uri가 채워진 경우 } if (showBottomSheet) { EditProfileBottomSheet( onDismiss = { showBottomSheet = false }, onSelectImage = { launchImagePicker() }, - onDefaultImage = { imageUri = null } + onDefaultImage = { + useDefaultImage = true // 기본 이미지 + imageUri = null + isChanged = true + } ) } @@ -104,29 +120,87 @@ fun EditProfileScreen( onBack = { onAction(EditProfileAction.BackClicked) }, onSave = { val file = imageUri?.toFile(context, "profileImage") - Log.d("test", file?.name.toString()) - /** TODO: 저장 로직 */ - onAction(EditProfileAction.SaveClicked) + + onAction.invoke( + EditProfileAction.UpdateProfile( + userId = user.id, + profileImage = file, + nickname = textFieldState.text.toString() + ) + ) } ) Spacer(modifier = Modifier.height(16.dp)) - imageUri?.let { - GlideImage( - imageModel = { imageUri }, + if (imageUri != null && !useDefaultImage) { + Box(modifier = Modifier.align(Alignment.CenterHorizontally)) { + GlideImage( + imageModel = { imageUri ?: user.profileImageUrl }, // 둘 중 하나는 not-null + modifier = Modifier + .noRippleClickable { showBottomSheet = true } + .size(120.dp) + .clip(CircleShape) + ) + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(40.dp) + .wavyStroke( + color = MSTheme.color.black, + cornerRadius = 20.dp, + fillColor = MSTheme.color.black, + amplitude = 1.dp, + spacing = 1.dp + ), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_edit), + contentDescription = "edit", + colorFilter = ColorFilter.tint(MSTheme.color.white) + ) + } + } + } else { + Box( modifier = Modifier - .noRippleClickable { showBottomSheet = true } - .size(128.dp) - .clip(CircleShape) - .align(Alignment.CenterHorizontally), - ) - } ?: Image( - modifier = Modifier - .noRippleClickable { showBottomSheet = true } - .size(128.dp) - .align(Alignment.CenterHorizontally), - painter = painterResource(R.drawable.img_empty_profile), - contentDescription = "Profile" - ) + .size(120.dp) + .background( + color = MSTheme.color.greyG1, + shape = CircleShape + ) + .align(Alignment.CenterHorizontally) + .noRippleClickable { showBottomSheet = true }, + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.size(48.dp), + painter = painterResource(R.drawable.ic_photo), + contentDescription = "photo", + colorFilter = ColorFilter.tint(MSTheme.color.greyG3) + ) + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(40.dp) + .wavyStroke( + color = MSTheme.color.black, + cornerRadius = 20.dp, + fillColor = MSTheme.color.black, + amplitude = 1.dp, + spacing = 1.dp + ), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_edit), + contentDescription = "edit", + colorFilter = ColorFilter.tint(MSTheme.color.white) + ) + } + } + } Spacer(modifier = Modifier.height(16.dp)) MSText( text = "닉네임", @@ -146,5 +220,16 @@ fun EditProfileScreen( @Preview @Composable fun EditProfileScreenPreview() { - EditProfileScreen(onAction = {}) -} + EditProfileScreen( + data = EditProfileData( + user = ProfileResponse( + id = 0L, + nickname = "", + profileImageUrl = "", + email = "", + isOnboarding = true, + ) + ), + onAction = {} + ) +} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/editprofile/EditProfileViewModel.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/editprofile/EditProfileViewModel.kt index 9f7cb7dc..42bf314b 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/editprofile/EditProfileViewModel.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/editprofile/EditProfileViewModel.kt @@ -1,23 +1,30 @@ package com.idiotfrogs.profile.editprofile -import com.idiotfrogs.util.base.BaseUiState +import androidx.compose.runtime.Immutable +import com.idiotfrogs.domain.usecase.user.GetMyProfileUseCase +import com.idiotfrogs.domain.usecase.user.UpdateMyProfileUseCase +import com.idiotfrogs.model.user.ProfileResponse import com.idiotfrogs.util.base.BaseViewModel +import com.idiotfrogs.util.base.DataUiState +import com.idiotfrogs.util.sideEffect.RefreshEvent +import com.idiotfrogs.util.sideEffect.RefreshSideEffect import dagger.hilt.android.lifecycle.HiltViewModel import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.viewmodel.container +import java.io.File import javax.inject.Inject @HiltViewModel class EditProfileViewModel @Inject constructor( - + private val getMyProfileUseCase: GetMyProfileUseCase, + private val updateMyProfileUseCase: UpdateMyProfileUseCase, ) : BaseViewModel() { override val container: Container = container( initialState = EditProfileUiState(), onCreate = { - // TODO 초기 데이터 로딩 or 네비게이션에서 response 받아오기 safeLaunch { - intent { reduce { state.copy(isLoading = false, errorMessage = null) } } + fetchProfile() } } ) @@ -25,21 +32,73 @@ class EditProfileViewModel @Inject constructor( override fun onAction(action: EditProfileAction) { when (action) { EditProfileAction.BackClicked -> intent { postSideEffect(EditProfileSideEffect.NavigateToBack) } - EditProfileAction.SaveClicked -> intent { postSideEffect(EditProfileSideEffect.NavigateToBack) } + is EditProfileAction.UpdateProfile -> { + updateProfile(action.userId, action.profileImage, action.nickname) + } + } + } + + private fun fetchProfile() { + safeLaunch { + intent { reduce { state.copy(isLoading = true) } } + val result = getMyProfileUseCase() + + intent { + if (result.isFailure) { + reduce { + state.copy( + isLoading = false, + errorMessage = (result.exceptionOrNull()?.message) + ) + } + } else { + reduce { + state.copy( + isLoading = false, + data = EditProfileData( + result.getOrNull() + ) + ) + } + } + } + } + } + + private fun updateProfile(userId: Long, profileImage: File?, nickname: String) { + safeLaunch { + updateMyProfileUseCase( + userId = userId, + profileImage = profileImage, + nickname = nickname + ) + .onSuccess { + RefreshSideEffect.tryEmit(RefreshEvent.Profile) + intent { postSideEffect(EditProfileSideEffect.NavigateToBack) } + } } } } +@Immutable data class EditProfileUiState( + override val data: EditProfileData? = null, override val isLoading: Boolean = false, override val errorMessage: String? = null, -) : BaseUiState +) : DataUiState + +@Immutable +data class EditProfileData( + val user: ProfileResponse? = null +) sealed interface EditProfileAction { data object BackClicked : EditProfileAction - data object SaveClicked : EditProfileAction + data class UpdateProfile( + val userId: Long, val profileImage: File?, val nickname: String + ) : EditProfileAction } sealed interface EditProfileSideEffect { data object NavigateToBack : EditProfileSideEffect -} +} \ No newline at end of file diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileScreen.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileScreen.kt index f8f845d2..c6c6ac6a 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileScreen.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileScreen.kt @@ -15,23 +15,32 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.idiotfrogs.designsystem.component.MSLoadingOverlay +import com.idiotfrogs.designsystem.component.MSDashHorizontalDivider import com.idiotfrogs.designsystem.component.MSText import com.idiotfrogs.designsystem.theme.MSTheme import com.idiotfrogs.extension.toYearMonthDay +import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse +import com.idiotfrogs.model.timecapsule.TimeCapsuleRole +import com.idiotfrogs.model.timecapsule.TimeCapsuleStatus +import com.idiotfrogs.model.user.ProfileResponse import com.idiotfrogs.navigation.LocalComposeMSNavigator import com.idiotfrogs.navigation.Routes import com.idiotfrogs.profile.component.ProfileCard import com.idiotfrogs.profile.component.ProfileHeader import com.idiotfrogs.profile.component.ProfileTicketCard +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atTime +import kotlinx.datetime.todayIn import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect +import kotlin.time.Clock const val HeaderHeight = 56 @@ -47,6 +56,7 @@ fun ProfileRoute( ProfileSideEffect.NavigateToBack -> navigator.popBackStack() ProfileSideEffect.NavigateToEditProfile -> navigator.navigate(Routes.EditProfile) ProfileSideEffect.NavigateToSetting -> navigator.navigate(Routes.Setting) + is ProfileSideEffect.NavigateToDetail -> navigator.navigate(Routes.Detail(event.id)) } } @@ -80,7 +90,7 @@ fun ProfileScreen( LazyVerticalGrid( modifier = Modifier .fillMaxSize() - .background(Color(0xFFF6F6F6)) + .background(MSTheme.color.white) .padding(horizontal = 20.dp), columns = GridCells.Fixed(2), horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -90,7 +100,7 @@ fun ProfileScreen( ProfileCard( modifier = Modifier.padding(top = (HeaderHeight + 24).dp), nickname = data.user?.nickname ?: "", - imageUrl = data.user?.profileImageUrl, + imageUrl = data.user?.profileImageUrl?.ifEmpty { null }, onEditClick = { onAction(ProfileAction.EditProfileClicked) } ) } @@ -99,8 +109,16 @@ fun ProfileScreen( modifier = Modifier.padding(top = 16.dp), text = "오픈된 티켓", fontWeight = FontWeight.Bold, - fontSize = 20.dp, - color = MSTheme.color.greyG5 + fontSize = 16.dp, + color = MSTheme.color.greyG5, + textAlign = TextAlign.Center + ) + } + maxLineItem { + MSDashHorizontalDivider( + thickness = 2.dp, + dashWidth = 10.dp, + gapWidth = 10.dp ) } items(data.capsules) { @@ -108,7 +126,7 @@ fun ProfileScreen( imageUrl = it.mainImageUrl, title = it.title, date = it.createdAt.toYearMonthDay(), - onClick = {} + onClick = { onAction.invoke(ProfileAction.TicketClicked(it.timeCapsuleId))} ) } } @@ -128,7 +146,28 @@ private fun LazyGridScope.maxLineItem( @Composable private fun ProfileScreenPreview() { ProfileScreen( - data = ProfileData(), + data = ProfileData( + user = ProfileResponse( + id = 0L, + nickname = "용감한 사자처럼", + profileImageUrl = "", + email = "", + isOnboarding = true + ), + capsules = listOf( + MyTimeCapsuleResponse( + timeCapsuleId = 0L, + title = "제목입니다. 제목입니다.", + createdAt = Clock.System + .todayIn(TimeZone.currentSystemDefault()) + .atTime(0, 0, 0, 0), + mainImageUrl = "", + role = TimeCapsuleRole.CONTRIBUTOR, + timeCapsuleStatus = TimeCapsuleStatus.BURIED + + ) + ) + ), onAction = {}, ) } diff --git a/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileViewModel.kt b/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileViewModel.kt index 6f5a2342..1fa37d00 100644 --- a/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileViewModel.kt +++ b/feature/profile/src/main/java/com/idiotfrogs/profile/profile/ProfileViewModel.kt @@ -6,8 +6,8 @@ import com.idiotfrogs.domain.usecase.user.GetMyProfileUseCase import com.idiotfrogs.model.timecapsule.MyTimeCapsuleResponse import com.idiotfrogs.model.timecapsule.TimeCapsuleStatus import com.idiotfrogs.model.user.ProfileResponse -import com.idiotfrogs.util.base.DataUiState import com.idiotfrogs.util.base.BaseViewModel +import com.idiotfrogs.util.base.DataUiState import com.idiotfrogs.util.sideEffect.RefreshEvent import com.idiotfrogs.util.sideEffect.RefreshSideEffect import dagger.hilt.android.lifecycle.HiltViewModel @@ -77,6 +77,7 @@ class ProfileViewModel @Inject constructor( ProfileAction.EditProfileClicked -> intent { postSideEffect(ProfileSideEffect.NavigateToEditProfile) } ProfileAction.SettingClicked -> intent { postSideEffect(ProfileSideEffect.NavigateToSetting) } ProfileAction.BackClicked -> intent { postSideEffect(ProfileSideEffect.NavigateToBack) } + is ProfileAction.TicketClicked -> intent { postSideEffect(ProfileSideEffect.NavigateToDetail(action.id))} } } } @@ -98,10 +99,12 @@ sealed interface ProfileAction { data object SettingClicked : ProfileAction data object EditProfileClicked : ProfileAction data object BackClicked : ProfileAction + data class TicketClicked(val id: Long) : ProfileAction } sealed interface ProfileSideEffect { data object NavigateToSetting : ProfileSideEffect data object NavigateToEditProfile : ProfileSideEffect data object NavigateToBack : ProfileSideEffect + data class NavigateToDetail(val id: Long) : ProfileSideEffect } diff --git a/feature/profile/stability/profile-debug.stability b/feature/profile/stability/profile-debug.stability index be27ddb2..3ac14264 100644 --- a/feature/profile/stability/profile-debug.stability +++ b/feature/profile/stability/profile-debug.stability @@ -57,13 +57,14 @@ public fun com.idiotfrogs.profile.editprofile.EditProfileRoute(viewModel: com.id skippable: false restartable: true params: - - viewModel: RUNTIME (requires runtime check) + - viewModel: UNSTABLE (has mutable properties or unstable members) @Composable -public fun com.idiotfrogs.profile.editprofile.EditProfileScreen(onAction: kotlin.Function1): kotlin.Unit +public fun com.idiotfrogs.profile.editprofile.EditProfileScreen(data: com.idiotfrogs.profile.editprofile.EditProfileData, onAction: kotlin.Function1): kotlin.Unit skippable: true restartable: true params: + - data: STABLE (marked @Stable or @Immutable) - onAction: STABLE (function type) @Composable diff --git a/feature/profile/stability/profile-release.stability b/feature/profile/stability/profile-release.stability index be27ddb2..3ac14264 100644 --- a/feature/profile/stability/profile-release.stability +++ b/feature/profile/stability/profile-release.stability @@ -57,13 +57,14 @@ public fun com.idiotfrogs.profile.editprofile.EditProfileRoute(viewModel: com.id skippable: false restartable: true params: - - viewModel: RUNTIME (requires runtime check) + - viewModel: UNSTABLE (has mutable properties or unstable members) @Composable -public fun com.idiotfrogs.profile.editprofile.EditProfileScreen(onAction: kotlin.Function1): kotlin.Unit +public fun com.idiotfrogs.profile.editprofile.EditProfileScreen(data: com.idiotfrogs.profile.editprofile.EditProfileData, onAction: kotlin.Function1): kotlin.Unit skippable: true restartable: true params: + - data: STABLE (marked @Stable or @Immutable) - onAction: STABLE (function type) @Composable