diff --git a/app/src/main/java/com/poti/android/data/mapper/user/MyPageMapper.kt b/app/src/main/java/com/poti/android/data/mapper/user/MyPageMapper.kt new file mode 100644 index 00000000..2367ef9a --- /dev/null +++ b/app/src/main/java/com/poti/android/data/mapper/user/MyPageMapper.kt @@ -0,0 +1,37 @@ +package com.poti.android.data.mapper.user + +import com.poti.android.data.remote.dto.response.user.MyPageResponseDto +import com.poti.android.data.remote.dto.response.user.ParticipationSummaryDto +import com.poti.android.data.remote.dto.response.user.RecruitSummaryDto +import com.poti.android.domain.model.user.HistorySummary +import com.poti.android.domain.model.user.UserMyPage +import java.lang.String.format +import java.util.Locale + +fun MyPageResponseDto.toDomain(): UserMyPage = + UserMyPage( + nickname = nickname, + email = email, + profileImageUrl = profileImageUrl, + ratingAvg = format(Locale.US, "%.1f", ratingAvg), + activityMessage = activityMessage, + joinedAt = joinedAt, + hasFavoriteArtist = hasFavoriteArtist, + favoriteArtistName = favoriteArtistName, + participationSummary = participationSummary.toDomain(), + recruitSummary = recruitSummary.toDomain(), + ) + +fun ParticipationSummaryDto.toDomain(): HistorySummary = + HistorySummary( + total = total, + inProgress = inProgress, + completed = completed, + ) + +fun RecruitSummaryDto.toDomain(): HistorySummary = + HistorySummary( + total = total, + inProgress = inProgress, + completed = completed, + ) diff --git a/app/src/main/java/com/poti/android/data/remote/datasource/UserRemoteDataSource.kt b/app/src/main/java/com/poti/android/data/remote/datasource/UserRemoteDataSource.kt index 9e11c377..ee0c4768 100644 --- a/app/src/main/java/com/poti/android/data/remote/datasource/UserRemoteDataSource.kt +++ b/app/src/main/java/com/poti/android/data/remote/datasource/UserRemoteDataSource.kt @@ -3,6 +3,7 @@ package com.poti.android.data.remote.datasource import com.poti.android.core.network.model.BaseResponse import com.poti.android.data.remote.dto.request.user.NicknameDuplicateRequestDto import com.poti.android.data.remote.dto.request.user.OnboardingRequestDto +import com.poti.android.data.remote.dto.response.user.MyPageResponseDto import com.poti.android.data.remote.dto.response.user.NicknameDuplicateResponseDto import com.poti.android.data.remote.dto.response.user.OnboardingResponseDto import com.poti.android.data.remote.service.UserService @@ -16,4 +17,7 @@ class UserRemoteDataSource @Inject constructor( suspend fun postNicknameDuplicate(nicknameDuplicateRequest: NicknameDuplicateRequestDto): BaseResponse = userService.postNicknameDuplicate(nicknameDuplicateRequest = nicknameDuplicateRequest) + + suspend fun getUserMyPage(): BaseResponse = + userService.getUserMyPage() } diff --git a/app/src/main/java/com/poti/android/data/remote/dto/response/user/MyPageResponseDto.kt b/app/src/main/java/com/poti/android/data/remote/dto/response/user/MyPageResponseDto.kt new file mode 100644 index 00000000..4d74f53d --- /dev/null +++ b/app/src/main/java/com/poti/android/data/remote/dto/response/user/MyPageResponseDto.kt @@ -0,0 +1,50 @@ +package com.poti.android.data.remote.dto.response.user + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MyPageResponseDto( + @SerialName("userId") + val userId: Long, + @SerialName("nickname") + val nickname: String, + @SerialName("email") + val email: String, + @SerialName("profileImageUrl") + val profileImageUrl: String?, + @SerialName("ratingAvg") + val ratingAvg: Double, + @SerialName("activityMessage") + val activityMessage: String, + @SerialName("joinedAt") + val joinedAt: String, + @SerialName("hasFavoriteArtist") + val hasFavoriteArtist: Boolean, + @SerialName("favoriteArtistName") + val favoriteArtistName: String?, + @SerialName("participationSummary") + val participationSummary: ParticipationSummaryDto, + @SerialName("recruitSummary") + val recruitSummary: RecruitSummaryDto, +) + +@Serializable +data class ParticipationSummaryDto( + @SerialName("total") + val total: Int, + @SerialName("inProgress") + val inProgress: Int, + @SerialName("completed") + val completed: Int, +) + +@Serializable +data class RecruitSummaryDto( + @SerialName("total") + val total: Int, + @SerialName("inProgress") + val inProgress: Int, + @SerialName("completed") + val completed: Int, +) diff --git a/app/src/main/java/com/poti/android/data/remote/service/UserService.kt b/app/src/main/java/com/poti/android/data/remote/service/UserService.kt index 24d46861..5144fa14 100644 --- a/app/src/main/java/com/poti/android/data/remote/service/UserService.kt +++ b/app/src/main/java/com/poti/android/data/remote/service/UserService.kt @@ -3,9 +3,11 @@ package com.poti.android.data.remote.service import com.poti.android.core.network.model.BaseResponse import com.poti.android.data.remote.dto.request.user.NicknameDuplicateRequestDto import com.poti.android.data.remote.dto.request.user.OnboardingRequestDto +import com.poti.android.data.remote.dto.response.user.MyPageResponseDto import com.poti.android.data.remote.dto.response.user.NicknameDuplicateResponseDto import com.poti.android.data.remote.dto.response.user.OnboardingResponseDto import retrofit2.http.Body +import retrofit2.http.GET import retrofit2.http.PATCH import retrofit2.http.POST @@ -19,4 +21,7 @@ interface UserService { suspend fun postNicknameDuplicate( @Body nicknameDuplicateRequest: NicknameDuplicateRequestDto, ): BaseResponse + + @GET("/api/v1/users/mypage") + suspend fun getUserMyPage(): BaseResponse } diff --git a/app/src/main/java/com/poti/android/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/poti/android/data/repository/UserRepositoryImpl.kt index b8a11021..d66bc39d 100644 --- a/app/src/main/java/com/poti/android/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/poti/android/data/repository/UserRepositoryImpl.kt @@ -2,9 +2,12 @@ package com.poti.android.data.repository import com.poti.android.core.network.model.handleApiResponse import com.poti.android.core.network.util.HttpResponseHandler +import com.poti.android.data.mapper.artist.toDomain +import com.poti.android.data.mapper.user.toDomain import com.poti.android.data.remote.datasource.UserRemoteDataSource import com.poti.android.data.remote.dto.request.user.NicknameDuplicateRequestDto import com.poti.android.data.remote.dto.request.user.OnboardingRequestDto +import com.poti.android.domain.model.user.UserMyPage import com.poti.android.domain.repository.UserRepository import javax.inject.Inject @@ -34,4 +37,11 @@ class UserRepositoryImpl @Inject constructor( .getOrThrow() .isDuplicated } + + override suspend fun getUserMyPage(): Result = httpResponseHandler.safeApiCall { + userRemoteDataSource.getUserMyPage() + .handleApiResponse() + .getOrThrow() + .toDomain() + } } diff --git a/app/src/main/java/com/poti/android/domain/repository/UserRepository.kt b/app/src/main/java/com/poti/android/domain/repository/UserRepository.kt index 0f454ec4..f829d1cc 100644 --- a/app/src/main/java/com/poti/android/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/poti/android/domain/repository/UserRepository.kt @@ -1,5 +1,7 @@ package com.poti.android.domain.repository +import com.poti.android.domain.model.user.UserMyPage + interface UserRepository { suspend fun patchOnboarding( nickname: String, @@ -9,4 +11,6 @@ interface UserRepository { suspend fun postNicknameDuplicate( nickname: String, ): Result + + suspend fun getUserMyPage(): Result } diff --git a/app/src/main/java/com/poti/android/presentation/history/list/HistoryListScreen.kt b/app/src/main/java/com/poti/android/presentation/history/list/HistoryListScreen.kt index f88d6e8f..301a19e8 100644 --- a/app/src/main/java/com/poti/android/presentation/history/list/HistoryListScreen.kt +++ b/app/src/main/java/com/poti/android/presentation/history/list/HistoryListScreen.kt @@ -35,7 +35,9 @@ import com.poti.android.presentation.history.list.model.HistoryListUiState import com.poti.android.presentation.history.mapper.color import com.poti.android.presentation.history.mapper.labelResId import com.poti.android.presentation.history.mapper.statusColor +import kotlinx.serialization.Serializable +@Serializable enum class HistoryMode { RECRUIT, PARTICIPATION, diff --git a/app/src/main/java/com/poti/android/presentation/history/navigation/HistoryNavigation.kt b/app/src/main/java/com/poti/android/presentation/history/navigation/HistoryNavigation.kt index 63aa205d..b1d70fa5 100644 --- a/app/src/main/java/com/poti/android/presentation/history/navigation/HistoryNavigation.kt +++ b/app/src/main/java/com/poti/android/presentation/history/navigation/HistoryNavigation.kt @@ -8,15 +8,20 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.poti.android.core.navigation.Route import com.poti.android.presentation.history.list.HistoryListRoute +import com.poti.android.presentation.history.list.HistoryMode import com.poti.android.presentation.history.manage.ParticipantManageRoute import com.poti.android.presentation.history.participant.ParticipantDetailRoute import com.poti.android.presentation.history.recruiter.RecruiterDetailRoute import com.poti.android.presentation.party.detail.navigation.navigateToPartyDetail +import com.poti.android.presentation.user.component.HistorySummaryType import kotlinx.serialization.Serializable sealed interface HistoryRoute : Route { @Serializable - data object HistoryList : HistoryRoute + data class HistoryList( + val mode: HistoryMode? = HistoryMode.RECRUIT, + val type: HistorySummaryType? = HistorySummaryType.ALL, + ) : HistoryRoute @Serializable data class ParticipantDetail(val participantId: Long) : HistoryRoute @@ -28,8 +33,16 @@ sealed interface HistoryRoute : Route { data class ParticipantManage(val recruitId: Long) : HistoryRoute } -fun NavController.navigateToHistoryList() { - navigate(HistoryRoute.HistoryList) +fun NavController.navigateToHistoryList( + mode: HistoryMode, + type: HistorySummaryType, +) { + navigate( + HistoryRoute.HistoryList( + mode = mode, + type = type, + ), + ) } fun NavController.navigateToParticipantDetail(participantId: Long) { diff --git a/app/src/main/java/com/poti/android/presentation/main/MainTab.kt b/app/src/main/java/com/poti/android/presentation/main/MainTab.kt index 1d9af4ae..7aac5f56 100644 --- a/app/src/main/java/com/poti/android/presentation/main/MainTab.kt +++ b/app/src/main/java/com/poti/android/presentation/main/MainTab.kt @@ -21,7 +21,7 @@ enum class MainTab( MY_PARTY( iconResId = R.drawable.ic_history, label = R.string.bottom_nav_history, - route = HistoryRoute.HistoryList, + route = HistoryRoute.HistoryList(), ), MYPAGE( iconResId = R.drawable.ic_mypage, diff --git a/app/src/main/java/com/poti/android/presentation/user/component/HistorySummaryCard.kt b/app/src/main/java/com/poti/android/presentation/user/component/HistorySummaryCard.kt index 3f6091d5..60edb504 100644 --- a/app/src/main/java/com/poti/android/presentation/user/component/HistorySummaryCard.kt +++ b/app/src/main/java/com/poti/android/presentation/user/component/HistorySummaryCard.kt @@ -29,11 +29,13 @@ import com.poti.android.R import com.poti.android.core.common.extension.noRippleClickable import com.poti.android.core.designsystem.theme.PotiTheme import com.poti.android.domain.model.user.HistorySummary +import kotlinx.serialization.Serializable +@Serializable enum class HistorySummaryType { ALL, IN_PROGRESS, - FINISHED, + COMPLETED, } @Composable @@ -98,7 +100,7 @@ fun HistorySummaryCard( HistoryItem( title = stringResource(R.string.user_history_ended), count = summary.completed, - onClick = { onItemClick(HistorySummaryType.FINISHED) }, + onClick = { onItemClick(HistorySummaryType.COMPLETED) }, modifier = Modifier.weight(1f), ) } diff --git a/app/src/main/java/com/poti/android/presentation/user/mypage/MyPageScreen.kt b/app/src/main/java/com/poti/android/presentation/user/mypage/MyPageScreen.kt index 832059c8..c7b3d7a5 100644 --- a/app/src/main/java/com/poti/android/presentation/user/mypage/MyPageScreen.kt +++ b/app/src/main/java/com/poti/android/presentation/user/mypage/MyPageScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -27,8 +26,10 @@ import com.poti.android.core.designsystem.component.navigation.PotiHeaderPrimary import com.poti.android.core.designsystem.theme.PotiTheme import com.poti.android.domain.model.user.HistorySummary import com.poti.android.domain.model.user.UserMyPage +import com.poti.android.presentation.history.list.HistoryMode import com.poti.android.presentation.user.component.BadgeButton import com.poti.android.presentation.user.component.HistorySummaryCard +import com.poti.android.presentation.user.component.HistorySummaryType import com.poti.android.presentation.user.component.RatingBadge import com.poti.android.presentation.user.component.UserInfo import com.poti.android.presentation.user.component.UserProfile @@ -38,6 +39,7 @@ import com.poti.android.presentation.user.mypage.model.MyPageUiIntent @Composable fun MyPageRoute( onNavigateToArtist: () -> Unit, + onNavigateToHistoryList: (HistoryMode, HistorySummaryType) -> Unit, modifier: Modifier = Modifier, viewModel: MyPageViewModel = hiltViewModel(), ) { @@ -46,6 +48,10 @@ fun MyPageRoute( HandleSideEffects(viewModel.sideEffect) { effect -> when (effect) { MyPageUiEffect.NavigateToArtist -> onNavigateToArtist() + + is MyPageUiEffect.NavigateToHistoryList -> { + onNavigateToHistoryList(effect.mode, effect.tab) + } } } @@ -53,6 +59,11 @@ fun MyPageRoute( MyPageScreen( userMyPage = userMyPage, onArtistClick = { viewModel.processIntent(MyPageUiIntent.OnArtistClick) }, // TODO: [예림] 선택 최애 없을 때만 이동 + onHistoryClick = { mode, type -> + viewModel.processIntent( + MyPageUiIntent.OnHistoryClick(mode, type), + ) + }, modifier = modifier, ) } @@ -62,28 +73,25 @@ fun MyPageRoute( private fun MyPageScreen( userMyPage: UserMyPage, onArtistClick: () -> Unit, + onHistoryClick: (HistoryMode, HistorySummaryType) -> Unit, modifier: Modifier = Modifier, ) { val scrollState = rememberScrollState() val biasText = userMyPage.favoriteArtistName ?: stringResource(R.string.user_select_favorite_artist) - Scaffold( + Column( modifier = modifier.fillMaxSize(), - topBar = { - PotiHeaderPrimary( - title = stringResource(R.string.user_my_page_title), - firstIconRes = R.drawable.ic_setting, - onFirstIconClick = {}, - secondIconRes = R.drawable.ic_alarm, - onSecondIconClick = {}, - ) - }, - ) { innerPadding -> - + ) { + PotiHeaderPrimary( + title = stringResource(R.string.user_my_page_title), + firstIconRes = R.drawable.ic_setting, + onFirstIconClick = {}, + secondIconRes = R.drawable.ic_alarm, + onSecondIconClick = {}, + ) Column( modifier = Modifier .fillMaxSize() - .padding(innerPadding) .verticalScroll(scrollState) .padding( horizontal = screenWidthDp(16.dp), @@ -123,14 +131,14 @@ private fun MyPageScreen( HistorySummaryCard( title = stringResource(R.string.user_history_participate), summary = userMyPage.participationSummary, - onItemClick = { type -> }, // TODO: [예림] 분철 내역 뷰 연결 + onItemClick = { type -> onHistoryClick(HistoryMode.PARTICIPATION, type) }, modifier = Modifier.fillMaxWidth(), ) HistorySummaryCard( title = stringResource(R.string.user_history_recruit), summary = userMyPage.recruitSummary, - onItemClick = { type -> }, // TODO: [예림] 분철 내역 뷰 연결 + onItemClick = { type -> onHistoryClick(HistoryMode.RECRUIT, type) }, modifier = Modifier.fillMaxWidth(), ) } @@ -163,6 +171,7 @@ private fun ProfileScreenPreview() { ), ), onArtistClick = {}, + onHistoryClick = { _, _ -> }, modifier = Modifier, ) } @@ -194,6 +203,7 @@ private fun ProfileScreenPreview2() { ), ), onArtistClick = {}, + onHistoryClick = { _, _ -> }, modifier = Modifier, ) } diff --git a/app/src/main/java/com/poti/android/presentation/user/mypage/MyPageViewModel.kt b/app/src/main/java/com/poti/android/presentation/user/mypage/MyPageViewModel.kt index 67287c06..8cd0f827 100644 --- a/app/src/main/java/com/poti/android/presentation/user/mypage/MyPageViewModel.kt +++ b/app/src/main/java/com/poti/android/presentation/user/mypage/MyPageViewModel.kt @@ -2,8 +2,7 @@ package com.poti.android.presentation.user.mypage import com.poti.android.core.base.BaseViewModel import com.poti.android.core.common.state.ApiState -import com.poti.android.domain.model.user.HistorySummary -import com.poti.android.domain.model.user.UserMyPage +import com.poti.android.domain.repository.UserRepository import com.poti.android.presentation.user.mypage.model.MyPageUiEffect import com.poti.android.presentation.user.mypage.model.MyPageUiIntent import com.poti.android.presentation.user.mypage.model.MyPageUiState @@ -11,12 +10,23 @@ import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class MyPageViewModel @Inject constructor() : BaseViewModel( - initialState = MyPageUiState(), -) { +class MyPageViewModel @Inject constructor( + private val userRepository: UserRepository, +) : BaseViewModel( + initialState = MyPageUiState(), + ) { override fun processIntent(intent: MyPageUiIntent) { when (intent) { MyPageUiIntent.OnArtistClick -> sendEffect(MyPageUiEffect.NavigateToArtist) + + is MyPageUiIntent.OnHistoryClick -> { + sendEffect( + MyPageUiEffect.NavigateToHistoryList( + mode = intent.mode, + tab = intent.tab, + ), + ) + } } } @@ -24,32 +34,19 @@ class MyPageViewModel @Inject constructor() : BaseViewModel + updateState { + copy(userMyPageLoadState = ApiState.Success(userMyPage)) + } + } + .onFailure { throwable -> + updateState { + copy( + userMyPageLoadState = ApiState.Failure(throwable.message ?: "Failed"), + ) + } + } } } diff --git a/app/src/main/java/com/poti/android/presentation/user/mypage/model/Contracts.kt b/app/src/main/java/com/poti/android/presentation/user/mypage/model/Contracts.kt index 3fecc922..659d10d3 100644 --- a/app/src/main/java/com/poti/android/presentation/user/mypage/model/Contracts.kt +++ b/app/src/main/java/com/poti/android/presentation/user/mypage/model/Contracts.kt @@ -5,6 +5,8 @@ import com.poti.android.core.base.UiIntent import com.poti.android.core.base.UiState import com.poti.android.core.common.state.ApiState import com.poti.android.domain.model.user.UserMyPage +import com.poti.android.presentation.history.list.HistoryMode +import com.poti.android.presentation.user.component.HistorySummaryType data class MyPageUiState( val userMyPageLoadState: ApiState = ApiState.Loading, @@ -12,8 +14,18 @@ data class MyPageUiState( sealed interface MyPageUiIntent : UiIntent { data object OnArtistClick : MyPageUiIntent + + data class OnHistoryClick( + val mode: HistoryMode, + val tab: HistorySummaryType, + ) : MyPageUiIntent } sealed interface MyPageUiEffect : UiEffect { data object NavigateToArtist : MyPageUiEffect + + data class NavigateToHistoryList( + val mode: HistoryMode, + val tab: HistorySummaryType, + ) : MyPageUiEffect } diff --git a/app/src/main/java/com/poti/android/presentation/user/mypage/navigation/MyPageNavigation.kt b/app/src/main/java/com/poti/android/presentation/user/mypage/navigation/MyPageNavigation.kt index 173b5fa0..ee769036 100644 --- a/app/src/main/java/com/poti/android/presentation/user/mypage/navigation/MyPageNavigation.kt +++ b/app/src/main/java/com/poti/android/presentation/user/mypage/navigation/MyPageNavigation.kt @@ -7,6 +7,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.poti.android.core.navigation.Route +import com.poti.android.presentation.history.navigation.navigateToHistoryList import com.poti.android.presentation.onboarding.navigation.navigateToOnboardingArtist import com.poti.android.presentation.user.mypage.MyPageRoute import kotlinx.serialization.Serializable @@ -27,6 +28,7 @@ fun NavGraphBuilder.myPageNavGraph( composable { MyPageRoute( onNavigateToArtist = navController::navigateToOnboardingArtist, + onNavigateToHistoryList = navController::navigateToHistoryList, modifier = Modifier.padding(paddingValues), ) }