diff --git a/app/src/main/java/com/whyranoid/walkie/KoinModules.kt b/app/src/main/java/com/whyranoid/walkie/KoinModules.kt index 301e712a..d0328026 100644 --- a/app/src/main/java/com/whyranoid/walkie/KoinModules.kt +++ b/app/src/main/java/com/whyranoid/walkie/KoinModules.kt @@ -96,6 +96,7 @@ import com.whyranoid.presentation.viewmodel.SearchFriendViewModel import com.whyranoid.presentation.viewmodel.SelectHistoryViewModel import com.whyranoid.presentation.viewmodel.SignInViewModel import com.whyranoid.presentation.viewmodel.SplashViewModel +import com.whyranoid.presentation.viewmodel.TotalBadgeViewModel import com.whyranoid.presentation.viewmodel.UserPageViewModel import com.whyranoid.presentation.viewmodel.challenge.ChallengeDetailViewModel import com.whyranoid.presentation.viewmodel.challenge.ChallengeExitViewModel @@ -130,6 +131,7 @@ val viewModelModule = viewModel { CommunityScreenViewModel(get(), get(), get()) } viewModel { FollowingViewModel(get(), get(), get(), get(), get(), get()) } viewModel { SettingViewModel(get(), get()) } + viewModel { TotalBadgeViewModel(get(), get()) } } val repositoryModule = diff --git a/data/src/main/java/com/whyranoid/data/datasource/account/AccountDataSourceImpl.kt b/data/src/main/java/com/whyranoid/data/datasource/account/AccountDataSourceImpl.kt index 97bc1f16..3afda1ff 100644 --- a/data/src/main/java/com/whyranoid/data/datasource/account/AccountDataSourceImpl.kt +++ b/data/src/main/java/com/whyranoid/data/datasource/account/AccountDataSourceImpl.kt @@ -14,6 +14,7 @@ import java.io.File class AccountDataSourceImpl(private val accountService: AccountService) : AccountDataSource { override suspend fun signUp( + name: String, nickName: String, profileUrl: String?, authId: String, @@ -23,6 +24,7 @@ class AccountDataSourceImpl(private val accountService: AccountService) : Accoun return kotlin.runCatching { val request = SignUpRequest( userName = nickName, + name = name, profileImg = profileUrl ?: "", authId = authId, agreeGps = agreeGps, diff --git a/data/src/main/java/com/whyranoid/data/model/account/SignUpRequest.kt b/data/src/main/java/com/whyranoid/data/model/account/SignUpRequest.kt index a1b4f474..d111ef2c 100644 --- a/data/src/main/java/com/whyranoid/data/model/account/SignUpRequest.kt +++ b/data/src/main/java/com/whyranoid/data/model/account/SignUpRequest.kt @@ -2,6 +2,7 @@ package com.whyranoid.data.model.account data class SignUpRequest( val userName: String, + val name: String, val profileImg: String, val authId: String, val agreeGps: Boolean, diff --git a/data/src/main/java/com/whyranoid/data/model/account/UserInfoResponse.kt b/data/src/main/java/com/whyranoid/data/model/account/UserInfoResponse.kt index ae354f3e..fb3faaae 100644 --- a/data/src/main/java/com/whyranoid/data/model/account/UserInfoResponse.kt +++ b/data/src/main/java/com/whyranoid/data/model/account/UserInfoResponse.kt @@ -3,12 +3,13 @@ package com.whyranoid.data.model.account import com.whyranoid.domain.model.account.UserInfo data class UserInfoResponse ( + val name: String, val nickname: String, val profileImg: String? ) fun UserInfoResponse.toUserInfo() = UserInfo( - name = "", // TODO 수정 + name = name, nickname = nickname, profileImg = profileImg ) \ No newline at end of file diff --git a/data/src/main/java/com/whyranoid/data/repository/AccountRepositoryImpl.kt b/data/src/main/java/com/whyranoid/data/repository/AccountRepositoryImpl.kt index e3a466c8..613feb2e 100644 --- a/data/src/main/java/com/whyranoid/data/repository/AccountRepositoryImpl.kt +++ b/data/src/main/java/com/whyranoid/data/repository/AccountRepositoryImpl.kt @@ -38,7 +38,7 @@ class AccountRepositoryImpl( agreeSubscription: Boolean, ): Result { return kotlin.runCatching { - accountDataSource.signUp(nickName, profileUrl, authId, agreeGps, agreeSubscription) + accountDataSource.signUp(userName, nickName, profileUrl, authId, agreeGps, agreeSubscription) .onSuccess { uid -> accountDataStore.updateUId(uid) accountDataStore.updateAuthId(authId) diff --git a/domain/src/main/java/com/whyranoid/domain/datasource/AccountDataSource.kt b/domain/src/main/java/com/whyranoid/domain/datasource/AccountDataSource.kt index 22ad5805..8b8dd4be 100644 --- a/domain/src/main/java/com/whyranoid/domain/datasource/AccountDataSource.kt +++ b/domain/src/main/java/com/whyranoid/domain/datasource/AccountDataSource.kt @@ -5,6 +5,7 @@ import com.whyranoid.domain.model.account.UserInfo interface AccountDataSource { suspend fun signUp( + name: String, nickName: String, profileUrl: String?, authId: String, diff --git a/domain/src/main/java/com/whyranoid/domain/model/challenge/BadgeInfo.kt b/domain/src/main/java/com/whyranoid/domain/model/challenge/BadgeInfo.kt new file mode 100644 index 00000000..3aaf57b8 --- /dev/null +++ b/domain/src/main/java/com/whyranoid/domain/model/challenge/BadgeInfo.kt @@ -0,0 +1,10 @@ +package com.whyranoid.domain.model.challenge + +import androidx.annotation.DrawableRes + +data class BadgeInfo ( + val id: Int, + @DrawableRes val image: Int, + val name: String, + val receivedAt: String = "" +) \ No newline at end of file diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index 04a8581a..701595be 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -19,7 +19,7 @@ android { defaultConfig { minSdk = 26 - targetSdk = 33 + targetSdk = 34 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") diff --git a/presentation/src/main/java/com/whyranoid/presentation/component/badge/PlaceHolderBadge.kt b/presentation/src/main/java/com/whyranoid/presentation/component/badge/PlaceHolderBadge.kt index 7081fcf3..7138762d 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/component/badge/PlaceHolderBadge.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/component/badge/PlaceHolderBadge.kt @@ -19,16 +19,18 @@ import androidx.compose.ui.platform.LocalDensity import com.whyranoid.presentation.theme.WalkieColor @Composable -fun PlaceholderBadge() { +fun BadgePlaceHolder( + modifier: Modifier = Modifier +) { Box( contentAlignment = Alignment.Center, - modifier = Modifier + modifier = modifier .size(48.dp) .clip(CircleShape) .background(WalkieColor.GrayDisable) ) { val pxValue = LocalDensity.current.run { 2.dp.toPx() } - Canvas(modifier = Modifier.size(48.dp)) { + Canvas(modifier = modifier.size(48.dp)) { drawCircle( color = Color.Gray.copy(alpha = 0.3f), style = Stroke( @@ -44,6 +46,6 @@ fun PlaceholderBadge() { @Preview fun PlaceholderBadgePreview() { WalkieTheme { - PlaceholderBadge() + BadgePlaceHolder() } } \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/component/button/WalkieNormalButton.kt b/presentation/src/main/java/com/whyranoid/presentation/component/button/WalkieNormalButton.kt new file mode 100644 index 00000000..9bcdb83c --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/component/button/WalkieNormalButton.kt @@ -0,0 +1,54 @@ +package com.whyranoid.presentation.component.button + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.whyranoid.presentation.theme.WalkieTheme + +@Composable +fun WalkieNormalButton( + buttonText: String, + onButtonClick: () -> Unit, +) { + Button( + onClick = { + onButtonClick() + }, + colors = ButtonDefaults.buttonColors( + contentColor = Color.Black, + disabledContentColor = Color.Black, + backgroundColor = Color(0xFFEEEEEE) + ), + modifier = Modifier + .fillMaxWidth() + .height(34.dp), + shape = RoundedCornerShape(7.dp), + elevation = null + ) { + Text( + text = buttonText, + style = TextStyle( + color = Color.Black, + fontSize = 14.sp + ) + ) + } +} + +@Preview +@Composable +fun WalkieNormalButtonPreview() { + WalkieTheme { + WalkieNormalButton("전체 뱃지 보기") {} + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/reusable/BadgeItem.kt b/presentation/src/main/java/com/whyranoid/presentation/reusable/BadgeItem.kt new file mode 100644 index 00000000..54c0fff8 --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/reusable/BadgeItem.kt @@ -0,0 +1,72 @@ +package com.whyranoid.presentation.reusable + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.whyranoid.domain.model.challenge.BadgeInfo +import com.whyranoid.presentation.R +import com.whyranoid.presentation.theme.WalkieTheme +import com.whyranoid.presentation.theme.WalkieTypography + +@Composable +fun BadgeItem( + badgeInfo: BadgeInfo +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(54.dp) + .align(Alignment.CenterHorizontally) + ) { + Image( + painter = painterResource(badgeInfo.image), + contentDescription = null + ) + } + + Spacer( + modifier = Modifier.height(4.dp) + ) + + Text( + text = badgeInfo.name, + textAlign = TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = WalkieTypography.Body2, + modifier = Modifier + .widthIn(max = 62.dp) + .heightIn(max = 24.dp) + ) + } +} + +@Preview +@Composable +private fun BadgePreview() { + WalkieTheme { + BadgeItem( + BadgeInfo( + 1, + R.drawable.badge_test_2, + "test" + ) + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/reusable/DragComposable.kt b/presentation/src/main/java/com/whyranoid/presentation/reusable/DragComposable.kt new file mode 100644 index 00000000..2774de44 --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/reusable/DragComposable.kt @@ -0,0 +1,147 @@ +package com.whyranoid.presentation.reusable + +import android.util.Log +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.zIndex + + +internal val LocalDragTargetInfo = compositionLocalOf { DragTargetInfo() } + +@Composable +fun DragTarget( + modifier: Modifier = Modifier, + dataToDrop: T, + currentState: DragTargetInfo, + content: @Composable (() -> Unit), + placeholder: @Composable (() -> Unit) +) { + var currentPosition by remember { mutableStateOf(Offset.Zero) } + var isDragging by remember { mutableStateOf(false) } + Log.d("sm.shin", "dragtarget: $dataToDrop") + currentState.dataToDrop = dataToDrop + + Box( + modifier = modifier + .zIndex(1f) + .onGloballyPositioned { + currentPosition = it.localToRoot(Offset.Zero) + } + .pointerInput(Unit) { + detectDragGesturesAfterLongPress( + onDragStart = { + Log.d("sm.shin", "onDragStart: $dataToDrop") + currentState.isDragging = true + currentState.dragPosition = currentPosition + currentState.draggableComposable = content + isDragging = true + }, onDrag = { change, dragAmount -> + change.consume() + currentState.dragOffset += Offset(dragAmount.x, dragAmount.y) + }, onDragEnd = { + currentState.isDragging = false + currentState.dragOffset = Offset.Zero + isDragging = false + }, onDragCancel = { + currentState.dragOffset = Offset.Zero + currentState.isDragging = false + isDragging = false + } + ) + } + ) { + if (isDragging) { + placeholder() + } else { + content() + } + } +} + +@Composable +fun LongPressDraggable( + modifier: Modifier = Modifier, + isDraggable: Boolean = true, + state: DragTargetInfo, + content: @Composable BoxScope.() -> Unit +) { + CompositionLocalProvider( + LocalDragTargetInfo provides state + ) { + Box( + modifier = modifier.fillMaxSize() + ) { + content() + if (state.isDragging && isDraggable) { + var targetSize by remember { + mutableStateOf(IntSize.Zero) + } + Box(modifier = Modifier + .graphicsLayer { + val offset = (state.dragPosition + state.dragOffset) + scaleX = 1.1f + scaleY = 1.1f + alpha = if (targetSize == IntSize.Zero) 0f else 1f + translationX = offset.x + translationY = offset.y + } + .onGloballyPositioned { + targetSize = it.size + } + ) { + state.draggableComposable?.invoke() + } + } + } + } +} + +@Composable +@Suppress("UNCHECKED_CAST") +fun DropTarget( + modifier: Modifier, + dragInfo: DragTargetInfo, + content: @Composable (BoxScope.(isInBound: Boolean, data: T?) -> Unit) +) { + val dragPosition = dragInfo.dragPosition + val dragOffset = dragInfo.dragOffset + var isCurrentDropTarget by remember { + mutableStateOf(false) + } + + Box(modifier = modifier.onGloballyPositioned { + it.boundsInWindow().let { rect -> + val offset = dragPosition + dragOffset + isCurrentDropTarget = rect.contains(Offset(offset.x + rect.width / 2, offset.y + rect.height / 2)) + } + } + ) { + val data = + if (isCurrentDropTarget && !dragInfo.isDragging) dragInfo.dataToDrop as T? else null + content(isCurrentDropTarget, data) + } +} + +class DragTargetInfo { + var isDragging: Boolean by mutableStateOf(false) + var dragPosition by mutableStateOf(Offset.Zero) + var dragOffset by mutableStateOf(Offset.Zero) + var draggableComposable by mutableStateOf<(@Composable () -> Unit)?>(null) + var dataToDrop by mutableStateOf(null) +} \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/reusable/MainBadgeItem.kt b/presentation/src/main/java/com/whyranoid/presentation/reusable/MainBadgeItem.kt new file mode 100644 index 00000000..0bae4ae4 --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/reusable/MainBadgeItem.kt @@ -0,0 +1,69 @@ +package com.whyranoid.presentation.reusable + +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CircleShape +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.res.painterResource +import androidx.compose.ui.unit.dp +import com.whyranoid.domain.model.challenge.BadgeInfo +import com.whyranoid.presentation.component.badge.BadgePlaceHolder + +@Composable +fun MainBadgeItem( + badgeInfo: BadgeInfo, + currentState: DragTargetInfo +) { + Log.d("sm.shin", "mainbadgeInfo: $badgeInfo") + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .wrapContentSize() + ) { + Box( + modifier = Modifier + .size(54.dp) + .align(Alignment.CenterHorizontally) + ) { + if (badgeInfo.image == null) { + BadgePlaceHolder( + modifier = Modifier + .size(45.dp) + .clip(CircleShape) + .background(Color(0xFFF7F7F7)) + .align(Alignment.Center) + ) + + } else { + DragTarget( + modifier = Modifier.size(54.dp), + dataToDrop = badgeInfo, + currentState = currentState, + content = { + Image( + painter = painterResource(id = badgeInfo.image), + contentDescription = null, + modifier = Modifier.size(54.dp) + ) + }, + placeholder = { + BadgePlaceHolder( + modifier = Modifier + .size(45.dp) + .align(Alignment.Center) + ) + } + ) + } + } + } +} diff --git a/presentation/src/main/java/com/whyranoid/presentation/reusable/WalkieTitleBar.kt b/presentation/src/main/java/com/whyranoid/presentation/reusable/WalkieTitleBar.kt new file mode 100644 index 00000000..c28f3a7e --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/reusable/WalkieTitleBar.kt @@ -0,0 +1,63 @@ +package com.whyranoid.presentation.reusable + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.whyranoid.presentation.R +import com.whyranoid.presentation.theme.WalkieTheme +import com.whyranoid.presentation.theme.WalkieTypography + +@Composable +fun WalkieTitleBar( + modifier: Modifier = Modifier, + title: String, + @DrawableRes backIcon: Int = R.drawable.ic_back_arrow, + trailings: @Composable BoxScope.() -> Unit = {}, + onBackClick: () -> Unit = {}, +) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + ) { + Icon( + painter = painterResource(id = backIcon), + contentDescription = null, + modifier = Modifier.clickable { onBackClick() } + ) + + Text( + text = title, + style = WalkieTypography.Title + ) + + Box { + trailings() + } + } +} + +@Preview +@Composable +private fun WalkieTitleBarPreview() { + WalkieTheme { + WalkieTitleBar( + title = "전체 뱃지" + ) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt index 95113a72..85e58c13 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt @@ -35,6 +35,7 @@ import com.whyranoid.presentation.screens.challenge.ChallengeMainScreen import com.whyranoid.presentation.screens.community.CommentScreen import com.whyranoid.presentation.screens.community.SearchFriendScreen import com.whyranoid.presentation.screens.mypage.MyPageScreen +import com.whyranoid.presentation.screens.mypage.TotalBadgeScreen import com.whyranoid.presentation.screens.mypage.UserPageScreen import com.whyranoid.presentation.screens.mypage.addpost.AddPostScreen import com.whyranoid.presentation.screens.mypage.editprofile.EditProfileScreen @@ -147,6 +148,10 @@ fun AppScreenContent( MyPageScreen(navController) } + composable(Screen.TotalBadgeScreen.route) { + TotalBadgeScreen(navController) + } + composable(Screen.AddPostScreen.route) { AddPostScreen(navController = navController) } diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/Screen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/Screen.kt index 7959db44..19081061 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/Screen.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/Screen.kt @@ -47,6 +47,10 @@ sealed class Screen( R.drawable.ic_mypage_screen_selected, ) + object TotalBadgeScreen: Screen( + route = "totalBadgeScreen" + ) + object AddPostScreen : Screen( route = "addPostScreen", ) diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/MyPageScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/MyPageScreen.kt index 858693ca..11ea8e81 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/MyPageScreen.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/MyPageScreen.kt @@ -58,7 +58,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination import coil.compose.AsyncImage import com.whyranoid.domain.util.EMPTY -import com.whyranoid.presentation.component.badge.PlaceholderBadge +import com.whyranoid.presentation.component.badge.BadgePlaceHolder import com.whyranoid.presentation.component.bar.WalkieTopBar import com.whyranoid.presentation.reusable.TextWithCountSpaceBetween import com.whyranoid.presentation.screens.Screen @@ -97,7 +97,10 @@ fun MyPageScreen( UserPageContent( nickname = null, - state, + state = state, + onTotalBadgePageClicked = { + navController.navigate(Screen.TotalBadgeScreen.route) + }, onPostCreateClicked = { navController.navigate(Screen.AddPostScreen.route) }, @@ -158,6 +161,7 @@ fun MyPageScreen( fun UserPageContent( nickname: String? = null, // 상대방 페이지인 경우에 존재, 마이페이지일 경우 null state: UserPageState, + onTotalBadgePageClicked: () -> Unit = {}, onPostPreviewClicked: (id: Long) -> Unit = {}, onPostCreateClicked: () -> Unit = {}, onProfileEditClicked: () -> Unit = {}, @@ -204,6 +208,7 @@ fun UserPageContent( .clip(shape = CircleShape) .size(64.dp), ) + Spacer(modifier = Modifier.width(20.dp)) Column( @@ -307,7 +312,7 @@ fun UserPageContent( .size(56.dp), ) } - repeat(5 - badgeList.size) { PlaceholderBadge() } + repeat(5 - badgeList.size) { BadgePlaceHolder() } } // 마이페이지인 경우 @@ -319,8 +324,8 @@ fun UserPageContent( .fillMaxWidth() .padding(horizontal = 20.dp) .clip(RoundedCornerShape(12.dp)) - .clickable(enabled = badgeList.size >= 5) { - // TODO 전체 뱃지 페이지로 이동 + .clickable(enabled = true) { + onTotalBadgePageClicked() } .background(WalkieColor.GrayBackground) .padding(vertical = 8.dp), diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/TotalBadgeScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/TotalBadgeScreen.kt new file mode 100644 index 00000000..8499ee0f --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/TotalBadgeScreen.kt @@ -0,0 +1,231 @@ +package com.whyranoid.presentation.screens.mypage + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import com.whyranoid.domain.model.challenge.BadgeInfo +import com.whyranoid.presentation.R +import com.whyranoid.presentation.reusable.BadgeItem +import com.whyranoid.presentation.reusable.DragTargetInfo +import com.whyranoid.presentation.reusable.DropTarget +import com.whyranoid.presentation.reusable.LongPressDraggable +import com.whyranoid.presentation.reusable.MainBadgeItem +import com.whyranoid.presentation.reusable.WalkieTitleBar +import com.whyranoid.presentation.theme.WalkieTheme +import com.whyranoid.presentation.theme.WalkieTypography +import com.whyranoid.presentation.viewmodel.TotalBadgeViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +fun TotalBadgeScreen( + navController: NavController +) { + val viewModel = koinViewModel() + + val dragTargetInfo = remember { DragTargetInfo() } + + LongPressDraggable(state = dragTargetInfo) { + + // TODO remove + val mainBadgeList = remember { + mutableStateListOf( + BadgeInfo(1, R.drawable.badge_test, "badge 1"), + BadgeInfo(2, R.drawable.badge_test, "badge 2"), + BadgeInfo(3, R.drawable.badge_test, "badge 3"), + BadgeInfo(4, R.drawable.badge_test, "badge 4"), + BadgeInfo(5, R.drawable.badge_test, "badge 5") + ) + } + + val badgeList = remember { + mutableStateListOf( + BadgeInfo(6, R.drawable.badge_test_2, "badge 6"), + BadgeInfo(7, R.drawable.badge_test_2, "badge 7"), + BadgeInfo(8, R.drawable.badge_test_2, "badge 8"), + BadgeInfo(9, R.drawable.badge_test_2, "badge 9"), + BadgeInfo(10, R.drawable.badge_test_2, "badge 10"), + BadgeInfo(11, R.drawable.badge_test_2, "badge 11"), + BadgeInfo(11, R.drawable.badge_test_2, "badge 12"), + BadgeInfo(11, R.drawable.badge_test_2, "badge 13"), + ) + } + + fun changeBadge(mainBadge: BadgeInfo, badge: BadgeInfo) { + val mainBadgeIndex = mainBadgeList.indexOf(mainBadge) + val badgeIndex = badgeList.indexOf(badge) + + mainBadgeList[mainBadgeIndex] = badge + badgeList[badgeIndex] = mainBadge + } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + item { + WalkieTitleBar( + title = stringResource(id = R.string.total_badge_title), + onBackClick = { navController.popBackStack() } + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = stringResource(id = R.string.setting_main_badge_title), + style = WalkieTypography.SubTitle + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(id = R.string.setting_main_badge_description), + style = WalkieTypography.Body2, + color = Color(0xFF808080) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + ElevatedCard( + elevation = CardDefaults.cardElevation( + defaultElevation = 6.dp + ), + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 13.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + mainBadgeList.forEach { + MainBadgeItem( + badgeInfo = it, + currentState = dragTargetInfo, + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(color = Color(0xFFF5F5F5), shape = RoundedCornerShape(8.dp)) + .fillMaxWidth() + .wrapContentHeight() + .padding(16.dp) + ) { + val badgeCountText = buildAnnotatedString { + withStyle( + SpanStyle( + color = Color(0xFFF45A00), + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + ) { + append( + stringResource( + id = R.string.current_badge_count, + mainBadgeList.size + badgeList.size + ) + ) + } + append(" / ") + + withStyle( + SpanStyle( + fontWeight = FontWeight.Bold + ) + ) { + append("16") + } + + append(stringResource(id = R.string.total_badge_count)) + } + + Text( + text = badgeCountText, + style = WalkieTypography.Body2, + color = Color(0xFF9C9C9C) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + val chunkedBadges = badgeList.chunked(4) // 뱃지를 4개씩 묶음 + + Column { + chunkedBadges.forEach { rowBadges -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 20.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + rowBadges.forEach { badge -> + var currentInfo by remember { mutableStateOf(badge) } + + DropTarget( + dragInfo = dragTargetInfo, + modifier = Modifier.wrapContentSize() + ) {isInBound: Boolean, data: BadgeInfo? -> + if (isInBound && !dragTargetInfo.isDragging) { + if (dragTargetInfo.dataToDrop is BadgeInfo) { + changeBadge(dragTargetInfo.dataToDrop as BadgeInfo, currentInfo) + currentInfo = dragTargetInfo.dataToDrop as BadgeInfo + } + } + BadgeItem(currentInfo) + } + } + } + } + } + } + } + } + } +} + +@Preview +@Composable +private fun PreviewTotalBadgePage() { + WalkieTheme { + TotalBadgeScreen(navController = rememberNavController()) + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/theme/Theme.kt b/presentation/src/main/java/com/whyranoid/presentation/theme/Theme.kt index 8279bab1..32de03dc 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/theme/Theme.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/theme/Theme.kt @@ -28,6 +28,7 @@ private val LightColorScheme = lightColorScheme( secondary = WalkieColor.Secondary, tertiary = WalkieColor.Tertiary, surface = Color.White, + surfaceTint = Color.White /* Other default colors to override background = Color(0xFFFFFBFE), diff --git a/presentation/src/main/java/com/whyranoid/presentation/viewmodel/TotalBadgeViewModel.kt b/presentation/src/main/java/com/whyranoid/presentation/viewmodel/TotalBadgeViewModel.kt new file mode 100644 index 00000000..297a14cd --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/viewmodel/TotalBadgeViewModel.kt @@ -0,0 +1,48 @@ +package com.whyranoid.presentation.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.whyranoid.domain.model.challenge.Badge +import com.whyranoid.domain.usecase.GetMyUidUseCase +import com.whyranoid.domain.usecase.GetUserBadgesUseCase +import com.whyranoid.presentation.model.UiState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container + +sealed interface TotalBadgeSideEffect + +data class TotalBadgeState( + val badges: UiState> = UiState.Idle +) + +class TotalBadgeViewModel( + private val getUserBadgesUseCase: GetUserBadgesUseCase, + private val getMyUidUseCase: GetMyUidUseCase +): ViewModel(), ContainerHost { + + override val container = + container(TotalBadgeState()) + + init { + viewModelScope.launch(Dispatchers.IO) { + getMyUidUseCase().onSuccess { uid -> + getBadges(uid) + } + } + } + + private suspend fun getBadges(uid: Long) = intent { + val result = getUserBadgesUseCase(uid) + result.onSuccess { badges -> + reduce { + state.copy( + badges = UiState.Success(badges), + ) + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/res/drawable/badge_test.png b/presentation/src/main/res/drawable/badge_test.png new file mode 100644 index 00000000..d57380ce Binary files /dev/null and b/presentation/src/main/res/drawable/badge_test.png differ diff --git a/presentation/src/main/res/drawable/badge_test_2.png b/presentation/src/main/res/drawable/badge_test_2.png new file mode 100644 index 00000000..e29ecc57 Binary files /dev/null and b/presentation/src/main/res/drawable/badge_test_2.png differ diff --git a/presentation/src/main/res/drawable/ic_back_arrow.xml b/presentation/src/main/res/drawable/ic_back_arrow.xml new file mode 100644 index 00000000..ba097974 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_back_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 2ef84703..61c1a0aa 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -1,9 +1,13 @@ - 런닝 커뮤니티 챌린지 마이페이지 + 전체 뱃지 + 대표 뱃지 설정 + 뱃지를 꾹 누르면 대표 뱃지를 편집할 수 있어요. + %d + 개 획득 \ No newline at end of file