diff --git a/app/src/main/java/org/sopt/official/feature/navigator/NavigatorProviderIntent.kt b/app/src/main/java/org/sopt/official/feature/navigator/NavigatorProviderIntent.kt index b3e568395..9a819d1b7 100644 --- a/app/src/main/java/org/sopt/official/feature/navigator/NavigatorProviderIntent.kt +++ b/app/src/main/java/org/sopt/official/feature/navigator/NavigatorProviderIntent.kt @@ -27,6 +27,7 @@ package org.sopt.official.feature.navigator import android.content.Context import android.content.Intent import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import org.sopt.official.common.navigator.DeepLinkType import org.sopt.official.common.navigator.NavigatorProvider import org.sopt.official.feature.attendance.AttendanceActivity @@ -41,7 +42,6 @@ import org.sopt.official.feature.notification.detail.NotificationDetailActivity import org.sopt.official.feature.schedule.ScheduleActivity import org.sopt.official.model.UserStatus import org.sopt.official.stamp.feature.navigation.SoptampMissionArgs -import javax.inject.Inject class NavigatorProviderIntent @Inject constructor( @param:ApplicationContext private val context: Context, @@ -114,7 +114,14 @@ class NavigatorProviderIntent @Inject constructor( } } - override fun getPokeNotificationActivityIntent(name: String) : Intent { + override fun getAppjamtampActivityIntent(): Intent { + return Intent(context, MainActivity::class.java).apply { + putExtra("isAppjamtampDeepLink", true) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + } + + override fun getPokeNotificationActivityIntent(name: String): Intent { return Intent(context, MainActivity::class.java).apply { putExtra("userStatus", name) putExtra("isPokeNotification", true) @@ -122,7 +129,7 @@ class NavigatorProviderIntent @Inject constructor( } } - override fun getPokeFriendListSummaryActivityIntent(name: String, friendType: String?) : Intent { + override fun getPokeFriendListSummaryActivityIntent(name: String, friendType: String?): Intent { return Intent(context, MainActivity::class.java).apply { putExtra("userStatus", name) putExtra("friendType", friendType) diff --git a/core/common/src/main/java/org/sopt/official/common/navigator/DeepLinkType.kt b/core/common/src/main/java/org/sopt/official/common/navigator/DeepLinkType.kt index 6d00d4cfd..de72c0f34 100644 --- a/core/common/src/main/java/org/sopt/official/common/navigator/DeepLinkType.kt +++ b/core/common/src/main/java/org/sopt/official/common/navigator/DeepLinkType.kt @@ -103,6 +103,11 @@ enum class DeepLinkType( override fun getIntent(context: Context, userStatus: UserStatus, deepLink: String) = userStatus.setIntent(navigator.getSoptampActivityIntent()) }, + APPJAMTAMP("appjamtamp") { + override fun getIntent(context: Context, userStatus: UserStatus, deepLink: String) = + userStatus.setIntent(navigator.getAppjamtampActivityIntent()) + }, + // TODO - 콕찌르기 이슈 해결되면 딥링크에서 home 제거해야 함 (서버 변경으로 home 제거) POKE_NOTIFICATION_LIST("home/poke/notification-list") { override fun getIntent(context: Context, userStatus: UserStatus, deepLink: String) = diff --git a/core/common/src/main/java/org/sopt/official/common/navigator/NavigatorProvider.kt b/core/common/src/main/java/org/sopt/official/common/navigator/NavigatorProvider.kt index dca6ee114..2aa0f6954 100644 --- a/core/common/src/main/java/org/sopt/official/common/navigator/NavigatorProvider.kt +++ b/core/common/src/main/java/org/sopt/official/common/navigator/NavigatorProvider.kt @@ -38,6 +38,7 @@ interface NavigatorProvider { fun getAdjustSentenceActivityIntent(): Intent fun getAttendanceActivityIntent(): Intent fun getSoptampActivityIntent(): Intent + fun getAppjamtampActivityIntent(): Intent fun getPokeNotificationActivityIntent(name: String): Intent fun getPokeFriendListSummaryActivityIntent(name: String, friendType: String?): Intent fun getPokeActivityIntent(userStatus: UserStatus): Intent diff --git a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampMissionsResponseDto.kt b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampMissionsResponseDto.kt index 5be22ffe5..87360c583 100644 --- a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampMissionsResponseDto.kt +++ b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampMissionsResponseDto.kt @@ -8,16 +8,16 @@ import org.sopt.official.domain.appjamtamp.entity.AppjamtampMissionListEntity @Serializable data class AppjamtampMissionsResponseDto( @SerialName("teamNumber") - val teamNumber: String, + val teamNumber: String?, @SerialName("teamName") - val teamName: String, + val teamName: String?, @SerialName("missions") val missions: List ) { fun toEntity(): AppjamtampMissionListEntity { return AppjamtampMissionListEntity( - teamNumber = teamNumber, - teamName = teamName, + teamNumber = teamNumber.orEmpty(), + teamName = teamName.orEmpty(), missions = missions.map { it.toEntity() } ) } diff --git a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampMyAppjamInfoResponseDto.kt b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampMyAppjamInfoResponseDto.kt index 58b054797..8071050d1 100644 --- a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampMyAppjamInfoResponseDto.kt +++ b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampMyAppjamInfoResponseDto.kt @@ -7,18 +7,18 @@ import org.sopt.official.domain.appjamtamp.entity.AppjamtampMyAppjamInfoEntity @Serializable data class AppjamtampMyAppjamInfoResponseDto( @SerialName("teamNumber") - val teamNumber: String, + val teamNumber: String?, @SerialName("teamName") - val teamName: String, + val teamName: String?, @SerialName("isAppjamJoined") val isAppjamJoined: Boolean ) { fun toEntity(): AppjamtampMyAppjamInfoEntity { return AppjamtampMyAppjamInfoEntity( - teamNumber = teamNumber, - teamName = teamName, + teamNumber = teamNumber.orEmpty(), + teamName = teamName.orEmpty(), isAppjamJoined = isAppjamJoined ) } diff --git a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampPostStampResponseDto.kt b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampPostStampResponseDto.kt index 90384fddc..6db77c15b 100644 --- a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampPostStampResponseDto.kt +++ b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampPostStampResponseDto.kt @@ -13,6 +13,10 @@ data class AppjamtampPostStampResponseDto( val images: List, @SerialName("activityDate") val activityDate: String, + @SerialName("ownerNickname") + val ownerNickname: String = "", + @SerialName("ownerProfileImage") + val ownerProfileImage: String? = null, @SerialName("createdAt") val createdAt: String, @SerialName("updatedAt") diff --git a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampTop10MissionScoreResponse.kt b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampTop10MissionScoreResponse.kt index 4df7ee5b0..48d8de631 100644 --- a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampTop10MissionScoreResponse.kt +++ b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/dto/response/AppjamtampTop10MissionScoreResponse.kt @@ -13,13 +13,12 @@ data class AppjamtampTop10MissionScoreResponse( data class AppjamtampMissionScoreResponse( @SerialName("rank") val rank: Int, - @SerialName("teamName") val teamName: String, - + @SerialName("teamNumber") + val teamNumber: String, @SerialName("todayPoints") val todayPoints: Int, - @SerialName("totalPoints") val totalPoints: Int ) diff --git a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/mapper/AppjamptampMapper.kt b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/mapper/AppjamptampMapper.kt index 4e1bafaf5..f149b2c79 100644 --- a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/mapper/AppjamptampMapper.kt +++ b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/mapper/AppjamptampMapper.kt @@ -2,8 +2,8 @@ package org.sopt.official.data.appjamtamp.mapper import org.sopt.official.data.appjamtamp.dto.response.AppjamtampMissionScoreResponse import org.sopt.official.data.appjamtamp.dto.response.AppjamtampRecentMissionResponse -import org.sopt.official.data.appjamtamp.dto.response.AppjamtampTop3RecentMissionResponse import org.sopt.official.data.appjamtamp.dto.response.AppjamtampTop10MissionScoreResponse +import org.sopt.official.data.appjamtamp.dto.response.AppjamtampTop3RecentMissionResponse import org.sopt.official.domain.appjamtamp.entity.AppjamtampMissionScore import org.sopt.official.domain.appjamtamp.entity.AppjamtampRecentMission @@ -34,6 +34,7 @@ internal fun AppjamtampMissionScoreResponse.toDomain(): AppjamtampMissionScore { return AppjamtampMissionScore( rank = this.rank, teamName = this.teamName, + teamNumber = this.teamNumber, todayPoints = this.todayPoints, totalPoints = this.todayPoints ) diff --git a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/mapper/AppjamtampUserMapper.kt b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/mapper/AppjamtampUserMapper.kt new file mode 100644 index 000000000..b8921709c --- /dev/null +++ b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/mapper/AppjamtampUserMapper.kt @@ -0,0 +1,10 @@ +package org.sopt.official.data.appjamtamp.mapper + +import org.sopt.official.data.appjamtamp.dto.response.AppjamtampPostStampResponseDto +import org.sopt.official.domain.appjamtamp.entity.AppjamtampUser + +fun AppjamtampPostStampResponseDto.toDomain(): AppjamtampUser = + AppjamtampUser( + nickname = this.ownerNickname, + profileImage = this.ownerProfileImage + ) diff --git a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/repository/AppjamtampRepositoryImpl.kt b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/repository/AppjamtampRepositoryImpl.kt index 862671061..357502c93 100644 --- a/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/repository/AppjamtampRepositoryImpl.kt +++ b/data/appjamtamp/src/main/java/org/sopt/official/data/appjamtamp/repository/AppjamtampRepositoryImpl.kt @@ -10,6 +10,7 @@ import org.sopt.official.domain.appjamtamp.entity.AppjamtampMissionScore import org.sopt.official.domain.appjamtamp.entity.AppjamtampMyAppjamInfoEntity import org.sopt.official.domain.appjamtamp.entity.AppjamtampRecentMission import org.sopt.official.domain.appjamtamp.entity.AppjamtampStampEntity +import org.sopt.official.domain.appjamtamp.entity.AppjamtampUser import org.sopt.official.domain.appjamtamp.repository.AppjamtampRepository internal class AppjamtampRepositoryImpl @Inject constructor( @@ -34,13 +35,13 @@ internal class AppjamtampRepositoryImpl @Inject constructor( image: String, contents: String, activityDate: String - ): Result = suspendRunCatching { + ): Result = suspendRunCatching { appjamtampDataSource.postAppjamtampStamp( missionId = missionId, image = image, contents = contents, activityDate = activityDate - ) + ).toDomain() } override suspend fun getMyAppjamInfo(): Result = suspendRunCatching { diff --git a/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/entity/AppjamtampMissionScore.kt b/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/entity/AppjamtampMissionScore.kt index dbfdad832..b950cbcd0 100644 --- a/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/entity/AppjamtampMissionScore.kt +++ b/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/entity/AppjamtampMissionScore.kt @@ -3,6 +3,7 @@ package org.sopt.official.domain.appjamtamp.entity data class AppjamtampMissionScore( val rank: Int, val teamName: String, + val teamNumber: String, val todayPoints: Int, val totalPoints: Int ) diff --git a/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/entity/AppjamtampUser.kt b/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/entity/AppjamtampUser.kt new file mode 100644 index 000000000..dfb01bc4a --- /dev/null +++ b/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/entity/AppjamtampUser.kt @@ -0,0 +1,6 @@ +package org.sopt.official.domain.appjamtamp.entity + +data class AppjamtampUser( + val nickname: String, + val profileImage: String? +) diff --git a/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/repository/AppjamtampRepository.kt b/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/repository/AppjamtampRepository.kt index db36bcaa7..bf5aa2bf6 100644 --- a/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/repository/AppjamtampRepository.kt +++ b/domain/appjamtamp/src/main/java/org/sopt/official/domain/appjamtamp/repository/AppjamtampRepository.kt @@ -5,6 +5,7 @@ import org.sopt.official.domain.appjamtamp.entity.AppjamtampMissionScore import org.sopt.official.domain.appjamtamp.entity.AppjamtampMyAppjamInfoEntity import org.sopt.official.domain.appjamtamp.entity.AppjamtampRecentMission import org.sopt.official.domain.appjamtamp.entity.AppjamtampStampEntity +import org.sopt.official.domain.appjamtamp.entity.AppjamtampUser interface AppjamtampRepository { suspend fun getAppjamtampMissions( @@ -22,7 +23,7 @@ interface AppjamtampRepository { image: String, contents: String, activityDate: String - ): Result + ): Result suspend fun getMyAppjamInfo(): Result diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/component/MissionsGridComponent.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/component/MissionsGridComponent.kt index b858b9d61..08cc81892 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/component/MissionsGridComponent.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/component/MissionsGridComponent.kt @@ -2,8 +2,6 @@ package org.sopt.official.feature.appjamtamp.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items @@ -17,27 +15,24 @@ import org.sopt.official.feature.appjamtamp.missionlist.model.AppjamtampMissionU @Composable internal fun MissionsGridComponent( - missionList : ImmutableList, + missionList: ImmutableList, modifier: Modifier = Modifier, - onMissionItemClick: (item: AppjamtampMissionUiModel) -> Unit = {}, + onMissionItemClick: (item: AppjamtampMissionUiModel) -> Unit = {} ) { LazyVerticalGrid( modifier = modifier, columns = GridCells.Fixed(2), verticalArrangement = Arrangement.spacedBy(40.dp, Alignment.Top), horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(bottom = 80.dp), + contentPadding = PaddingValues(bottom = 80.dp) ) { items(items = missionList) { mission -> MissionComponent( mission = mission, onClick = { onMissionItemClick(mission) - }, + } ) } - item { - Spacer(modifier = Modifier.padding(vertical = 20.dp)) - } } -} \ No newline at end of file +} diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailRoute.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailRoute.kt index cf7077b97..9ff7be499 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailRoute.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailRoute.kt @@ -1,6 +1,7 @@ package org.sopt.official.feature.appjamtamp.missiondetail import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -30,8 +31,13 @@ import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import kotlinx.coroutines.delay import org.sopt.official.designsystem.SoptTheme import org.sopt.official.designsystem.component.dialog.TwoButtonDialog +import org.sopt.official.feature.appjamtamp.R import org.sopt.official.feature.appjamtamp.component.AppjamtampButton import org.sopt.official.feature.appjamtamp.component.BackButtonHeader import org.sopt.official.feature.appjamtamp.missiondetail.component.ClapFeedbackHolder @@ -43,6 +49,7 @@ import org.sopt.official.feature.appjamtamp.missiondetail.component.ImageContent import org.sopt.official.feature.appjamtamp.missiondetail.component.ImageModal import org.sopt.official.feature.appjamtamp.missiondetail.component.Memo import org.sopt.official.feature.appjamtamp.missiondetail.component.MissionHeader +import org.sopt.official.feature.appjamtamp.missiondetail.component.PostSubmissionBadge import org.sopt.official.feature.appjamtamp.missiondetail.component.ProfileTag import org.sopt.official.feature.appjamtamp.missiondetail.model.DetailViewType import org.sopt.official.feature.appjamtamp.model.ImageModel @@ -63,6 +70,29 @@ internal fun MissionDetailRoute( var selectedZoomInImage by remember { mutableStateOf(null) } var isDeleteDialogVisible by remember { mutableStateOf(false) } + var showPostSubmissionBadge by remember(uiState.showPostSubmissionBadge) { + mutableStateOf( + uiState.showPostSubmissionBadge + ) + } + + val lottieResId = remember(uiState.mission.level) { + when (uiState.mission.level.value) { + 1 -> R.raw.pinkstamps + 2 -> R.raw.purplestamp + 3 -> R.raw.greenstamp + else -> R.raw.orangestamp + } + } + + val lottieComposition by rememberLottieComposition( + spec = LottieCompositionSpec.RawRes(lottieResId), + ) + val progress by animateLottieCompositionAsState( + composition = lottieComposition, + isPlaying = showPostSubmissionBadge, + ) + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { viewModel.flushClap() } @@ -76,6 +106,13 @@ internal fun MissionDetailRoute( } } + LaunchedEffect(!uiState.isLoading, progress) { + if (progress >= 0.99f && !uiState.isLoading) { + delay(500L) + viewModel.updateShowPostSubmissionBadge() + } + } + if (uiState.viewType == DetailViewType.WRITE) { MyEmptyMissionDetailScreen( uiState = uiState, @@ -171,6 +208,13 @@ internal fun MissionDetailRoute( ) } } + + if (showPostSubmissionBadge) { + PostSubmissionBadge( + composition = lottieComposition, + progress = progress + ) + } } @Composable @@ -194,7 +238,9 @@ private fun MyEmptyMissionDetailScreen( ) { BackButtonHeader( title = "미션", - onBackButtonClick = onBackButtonClick + onBackButtonClick = onBackButtonClick, + modifier = Modifier + .padding(vertical = 12.dp) ) Spacer(modifier = Modifier.height(10.dp)) @@ -257,80 +303,89 @@ private fun MissionDetailScreen( val scrollState = rememberScrollState() var isEditable by remember(uiState.viewType) { mutableStateOf(uiState.viewType == DetailViewType.EDIT) } - Column( + Box( modifier = Modifier .fillMaxSize() - .systemBarsPadding() .padding(horizontal = 16.dp) - .verticalScroll(scrollState) + .systemBarsPadding(), + contentAlignment = Alignment.BottomCenter ) { - BackButtonHeader( - title = if (uiState.viewType == DetailViewType.COMPLETE) "내 미션" else uiState.teamName, - onBackButtonClick = onBackButtonClick, - trailingIcon = { - uiState.viewType.toolbarIcon?.let { - Icon( - imageVector = ImageVector.vectorResource(uiState.viewType.toolbarIcon), - contentDescription = null, - tint = SoptTheme.colors.onSurface10, - modifier = Modifier - .clickable(onClick = onToolbarIconClick) - ) - } - } - ) + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .align(Alignment.TopCenter) + ) { + BackButtonHeader( + title = if (uiState.viewType == DetailViewType.COMPLETE) "내 미션" else uiState.teamName, + onBackButtonClick = onBackButtonClick, + trailingIcon = { + uiState.viewType.toolbarIcon?.let { + Icon( + imageVector = ImageVector.vectorResource(uiState.viewType.toolbarIcon), + contentDescription = null, + tint = SoptTheme.colors.onSurface10, + modifier = Modifier + .clickable(onClick = onToolbarIconClick) + ) + } + }, + modifier = Modifier + .padding(vertical = 12.dp) + ) - Spacer(modifier = Modifier.height(10.dp)) + Spacer(modifier = Modifier.height(10.dp)) - MissionHeader( - title = uiState.mission.title, - stamp = Stamp.findStampByLevel(uiState.mission.level) - ) + MissionHeader( + title = uiState.mission.title, + stamp = Stamp.findStampByLevel(uiState.mission.level) + ) - Spacer(modifier = Modifier.height(5.dp)) + Spacer(modifier = Modifier.height(5.dp)) - ImageContent( - imageModel = uiState.imageModel, - onChangeImage = onChangeImage, - onClickZoomIn = onClickZoomIn, - isEditable = isEditable - ) + ImageContent( + imageModel = uiState.imageModel, + onChangeImage = onChangeImage, + onClickZoomIn = onClickZoomIn, + isEditable = isEditable + ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - ProfileTag( - name = uiState.writer.name, - profileImage = uiState.writer.profileImage - ) + ProfileTag( + name = uiState.writer.name, + profileImage = uiState.writer.profileImage + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - if (isEditable) { - DatePicker( - value = uiState.date, - placeHolder = "날짜를 입력해주세요.", - isEditable = true, - onClicked = onDatePickerClick - ) + if (isEditable) { + DatePicker( + value = uiState.date, + placeHolder = "날짜를 입력해주세요.", + isEditable = true, + onClicked = onDatePickerClick + ) - Spacer(modifier = Modifier.height(8.dp)) - } + Spacer(modifier = Modifier.height(8.dp)) + } - Memo( - value = uiState.content, - placeHolder = "함께한 사람과 어떤 추억을 남겼는지 작성해 주세요.", - onValueChange = onMemoChange, - isEditable = isEditable - ) - Spacer(modifier = Modifier.height(8.dp)) + Memo( + value = uiState.content, + placeHolder = "함께한 사람과 어떤 추억을 남겼는지 작성해 주세요.", + onValueChange = onMemoChange, + isEditable = isEditable + ) + Spacer(modifier = Modifier.height(8.dp)) - DetailInfo( - date = uiState.date, - clapCount = uiState.clapCount, - viewCount = uiState.viewCount - ) + DetailInfo( + date = uiState.date, + clapCount = uiState.clapCount, + viewCount = uiState.viewCount + ) - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(120.dp)) + } when (uiState.viewType) { DetailViewType.READ_ONLY -> { @@ -340,7 +395,6 @@ private fun MissionDetailScreen( onPressClap = onActionButtonClick, modifier = Modifier .fillMaxWidth() - .align(Alignment.CenterHorizontally) ) } diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailState.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailState.kt index cbb323876..3c60c21b5 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailState.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailState.kt @@ -12,7 +12,7 @@ internal data class MissionDetailState( val isLoading: Boolean = true, val isFailed: Boolean = false, - val viewType: DetailViewType = DetailViewType.WRITE, + val viewType: DetailViewType = DetailViewType.READ_ONLY, val mission: Mission = Mission.DEFAULT, val imageModel: ImageModel = ImageModel.Empty, val date: String = "", @@ -26,5 +26,6 @@ internal data class MissionDetailState( val unSyncedClapCount: Int = 0, val viewCount: Int = 0, - val clappers: ImmutableList = persistentListOf() + val clappers: ImmutableList = persistentListOf(), + val showPostSubmissionBadge: Boolean = false ) diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailViewModel.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailViewModel.kt index 05cf12d31..3f9ec5010 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailViewModel.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/MissionDetailViewModel.kt @@ -166,6 +166,12 @@ internal class MissionDetailViewModel @Inject constructor( } fun handleSubmit() { + with(_missionDetailState.value) { + if (content.isBlank()) return + if (imageModel is ImageModel.Empty) return + if (date.isBlank()) return + } + _missionDetailState.update { it.copy(isLoading = true) } @@ -188,11 +194,16 @@ internal class MissionDetailViewModel @Inject constructor( image = imageModel.url[0], contents = content, activityDate = date - ).onSuccess { + ).onSuccess { writer -> _missionDetailState.update { it.copy( isLoading = false, - viewType = DetailViewType.COMPLETE + viewType = DetailViewType.COMPLETE, + showPostSubmissionBadge = true, + writer = User( + writer.nickname, + writer.profileImage.orEmpty() + ) ) } }.onFailure { e -> @@ -332,4 +343,10 @@ internal class MissionDetailViewModel @Inject constructor( it.copy(content = value) } } + + fun updateShowPostSubmissionBadge() { + _missionDetailState.update { + it.copy(showPostSubmissionBadge = false) + } + } } diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/navigation/AppjamtampMissionDetailNavigation.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/navigation/AppjamtampMissionDetailNavigation.kt index 87e906ed6..8574b6438 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/navigation/AppjamtampMissionDetailNavigation.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missiondetail/navigation/AppjamtampMissionDetailNavigation.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable import org.sopt.official.core.navigation.Route @Serializable -data class AppjamtampMissionDetail( +internal data class AppjamtampMissionDetail( val missionId: Int = -1, val missionLevel: Int = 1, val title: String = "", diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/AppjamtampMissionScreen.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/AppjamtampMissionScreen.kt index e14054081..8cad54b61 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/AppjamtampMissionScreen.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/AppjamtampMissionScreen.kt @@ -1,18 +1,23 @@ package org.sopt.official.feature.appjamtamp.missionlist import android.content.Intent +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues 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.statusBarsPadding import androidx.compose.material3.FabPosition import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +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.platform.LocalContext @@ -25,6 +30,7 @@ import androidx.lifecycle.flowWithLifecycle import kotlinx.collections.immutable.persistentListOf import org.sopt.official.common.navigator.DeepLinkType import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.designsystem.component.dialog.OneButtonDialog import org.sopt.official.domain.appjamtamp.entity.MissionLevel import org.sopt.official.feature.appjamtamp.component.MissionsGridComponent import org.sopt.official.feature.appjamtamp.missionlist.component.AppjamtampDescription @@ -40,7 +46,8 @@ import org.sopt.official.webview.view.WebViewActivity @Composable internal fun AppjamtampMissionRoute( - paddingValues: PaddingValues, + navigateToMissionDetail: (missionId: Int, missionLevel: Int, title: String, ownerName: String?) -> Unit, + navigateToRanking: () -> Unit, viewModel: AppjamtampMissionViewModel = hiltViewModel() ) { val context = LocalContext.current @@ -48,6 +55,10 @@ internal fun AppjamtampMissionRoute( val state by viewModel.state.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { + viewModel.fetchAppjamMissions(state.currentMissionFilter.isCompleted) + } + LaunchedEffect(Unit) { viewModel.sideEffect.flowWithLifecycle(lifeCycleOwner.lifecycle) .collect { sideEffect -> @@ -70,29 +81,45 @@ internal fun AppjamtampMissionRoute( ) context.startActivity(intent) } + + is AppjamtampSideEffect.NavigateToMissionDetail -> { + navigateToMissionDetail( + sideEffect.mission.id, + sideEffect.mission.level.value, + sideEffect.mission.title, + sideEffect.mission.ownerName + ) + } + + AppjamtampSideEffect.NavigateToRanking -> navigateToRanking() } } } AppjamtampMissionScreen( - paddingValues = paddingValues, state = state, onReportButtonClick = viewModel::reportButtonClick, onEditMessageButtonClick = viewModel::onEditMessageButtonClick, onMenuClick = viewModel::updateMissionFilter, + onMissionClick = viewModel::onMissionItemClick, + onFloatingButtonClick = viewModel::onFloatingButtonClick ) } @Composable private fun AppjamtampMissionScreen( - paddingValues: PaddingValues, state: AppjamtampMissionState, onMenuClick: (String) -> Unit = {}, onReportButtonClick: () -> Unit = {}, - onEditMessageButtonClick: () -> Unit = {} + onEditMessageButtonClick: () -> Unit = {}, + onMissionClick: (mission: AppjamtampMissionUiModel) -> Unit = {}, + onFloatingButtonClick: () -> Unit = {} ) { + var isDialogVisible by remember { mutableStateOf(false) } + Scaffold( modifier = Modifier + .statusBarsPadding() .fillMaxSize(), containerColor = SoptTheme.colors.onSurface950, topBar = { @@ -107,9 +134,7 @@ private fun AppjamtampMissionScreen( }, floatingActionButton = { AppjamtampFloatingButton( - onClick = { - // Todo : 랭킹화면으로 이동 - } + onClick = onFloatingButtonClick ) }, floatingActionButtonPosition = FabPosition.Center @@ -117,29 +142,58 @@ private fun AppjamtampMissionScreen( Column( modifier = Modifier .fillMaxSize() - .padding(paddingValues) .padding(innerPadding) .padding(horizontal = 20.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(8.dp)) - AppjamtampDescription( - teamName = state.teamName, - modifier = Modifier - .fillMaxWidth() - ) + if (state.teamName.isNotBlank()) { + AppjamtampDescription( + teamName = state.teamName, + modifier = Modifier + .fillMaxWidth() + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) + } MissionsGridComponent( missionList = state.missionList.missionList, - onMissionItemClick = { item -> - // Todo : 미션 상세화면으로 이동 + onMissionItemClick = { + if (state.teamName.isNotBlank()) { + onMissionClick(it) + } else { + isDialogVisible = true + } } ) } } + + if (isDialogVisible) { + OneButtonDialog( + onDismiss = { isDialogVisible = false }, + buttonText = "확인", + onButtonClick = { isDialogVisible = false } + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "솝탬프 안내", + style = SoptTheme.typography.title18SB, + color = SoptTheme.colors.onSurface10 + ) + + Text( + text = "각 미션의 인증 내용은 개인, 앱잼팀 랭킹에서 확인해주세요.", + style = SoptTheme.typography.body14R, + color = SoptTheme.colors.onSurface100 + ) + } + } + } } @Preview(showBackground = true) @@ -196,7 +250,6 @@ private fun AppjamtampMissionScreenPreview() { ) AppjamtampMissionScreen( - paddingValues = PaddingValues(), state = mockState ) } diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/AppjamtampMissionViewModel.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/AppjamtampMissionViewModel.kt index bc14c0c9a..a166e9122 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/AppjamtampMissionViewModel.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/AppjamtampMissionViewModel.kt @@ -3,7 +3,7 @@ package org.sopt.official.feature.appjamtamp.missionlist import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toImmutableList +import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -12,12 +12,12 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.sopt.official.domain.appjamtamp.repository.AppjamtampRepository import org.sopt.official.domain.soptamp.repository.StampRepository +import org.sopt.official.feature.appjamtamp.missionlist.model.AppjamtampMissionUiModel import org.sopt.official.feature.appjamtamp.missionlist.model.toUiModel import org.sopt.official.feature.appjamtamp.missionlist.state.AppjamtampMissionState import org.sopt.official.feature.appjamtamp.missionlist.state.AppjamtampSideEffect import org.sopt.official.feature.appjamtamp.model.MissionFilter import timber.log.Timber -import javax.inject.Inject @HiltViewModel internal class AppjamtampMissionViewModel @Inject constructor( @@ -32,7 +32,6 @@ internal class AppjamtampMissionViewModel @Inject constructor( init { getReportUrl() - fetchAppjamMissions() } fun fetchAppjamMissions(isCompleted: Boolean? = null) { @@ -89,4 +88,18 @@ internal class AppjamtampMissionViewModel @Inject constructor( _sideEffect.emit(AppjamtampSideEffect.NavigateToEdit) } } -} \ No newline at end of file + + fun onMissionItemClick(mission: AppjamtampMissionUiModel) { + viewModelScope.launch { + if (_state.value.teamName.isNotBlank()) { + _sideEffect.emit(AppjamtampSideEffect.NavigateToMissionDetail(mission)) + } + } + } + + fun onFloatingButtonClick() { + viewModelScope.launch { + _sideEffect.emit(AppjamtampSideEffect.NavigateToRanking) + } + } +} diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/component/DropDownHeader.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/component/DropDownHeader.kt index 82a1e20fc..02ba0c07a 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/component/DropDownHeader.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/component/DropDownHeader.kt @@ -92,7 +92,7 @@ private fun DropDownMenuButton( onMenuClick: (String) -> Unit = {}, ) { var isMenuExpanded by remember { mutableStateOf(false) } - var selectedIndex by remember { mutableIntStateOf(0) } + var selectedIndex by remember { mutableIntStateOf(3) } Box { Icon( imageVector = ImageVector.vectorResource( diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/navigation/AppjamtampMissionList.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/navigation/AppjamtampMissionList.kt new file mode 100644 index 000000000..bcfcc8da5 --- /dev/null +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/navigation/AppjamtampMissionList.kt @@ -0,0 +1,7 @@ +package org.sopt.official.feature.appjamtamp.missionlist.navigation + +import kotlinx.serialization.Serializable +import org.sopt.official.core.navigation.Route + +@Serializable +internal data object AppjamtampMissionList : Route diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/state/AppjamtampContract.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/state/AppjamtampContract.kt index 596ea9e03..1ca891026 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/state/AppjamtampContract.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/missionlist/state/AppjamtampContract.kt @@ -1,16 +1,19 @@ package org.sopt.official.feature.appjamtamp.missionlist.state import org.sopt.official.feature.appjamtamp.missionlist.model.AppjamtampMissionListUiModel +import org.sopt.official.feature.appjamtamp.missionlist.model.AppjamtampMissionUiModel import org.sopt.official.feature.appjamtamp.model.MissionFilter data class AppjamtampMissionState( val reportUrl: String = "", val teamName: String = "", val missionList: AppjamtampMissionListUiModel = AppjamtampMissionListUiModel(), - val currentMissionFilter: MissionFilter = MissionFilter.ALL + val currentMissionFilter: MissionFilter = MissionFilter.APPJAM ) sealed interface AppjamtampSideEffect { data object NavigateToWebView : AppjamtampSideEffect data object NavigateToEdit : AppjamtampSideEffect -} \ No newline at end of file + data class NavigateToMissionDetail(val mission: AppjamtampMissionUiModel) : AppjamtampSideEffect + data object NavigateToRanking : AppjamtampSideEffect +} diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/model/MissionFilter.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/model/MissionFilter.kt index 90ad189fd..972ccaebc 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/model/MissionFilter.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/model/MissionFilter.kt @@ -2,19 +2,19 @@ package org.sopt.official.feature.appjamtamp.model import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import org.sopt.official.domain.soptamp.model.MissionsFilter -enum class MissionFilter ( - val text : String, - val isCompleted : Boolean? +enum class MissionFilter( + val text: String, + val isCompleted: Boolean? ) { - ALL("앱잼 미션", null), + ALL("전체 미션", null), COMPLETED("완료 미션", true), - UNCOMPLETED("미완료 미션", false); + UNCOMPLETED("미완료 미션", false), + APPJAM("앱잼 미션", null); companion object { - fun getTitleOfMissionsList(): ImmutableList = MissionsFilter.entries.map { it.title }.toImmutableList() - fun findFilterByText(text : String) : MissionFilter = + fun getTitleOfMissionsList(): ImmutableList = MissionFilter.entries.map { it.text }.toImmutableList() + fun findFilterByText(text: String): MissionFilter = entries.find { it.text == text } ?: ALL } -} \ No newline at end of file +} diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/navigation/AppjamtampNavGraph.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/navigation/AppjamtampNavGraph.kt new file mode 100644 index 000000000..436e78218 --- /dev/null +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/navigation/AppjamtampNavGraph.kt @@ -0,0 +1,60 @@ +package org.sopt.official.feature.appjamtamp.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation +import androidx.navigation.toRoute +import kotlinx.serialization.Serializable +import org.sopt.official.core.navigation.MainTabRoute +import org.sopt.official.feature.appjamtamp.missiondetail.MissionDetailRoute +import org.sopt.official.feature.appjamtamp.missiondetail.navigation.AppjamtampMissionDetail +import org.sopt.official.feature.appjamtamp.missionlist.AppjamtampMissionRoute +import org.sopt.official.feature.appjamtamp.missionlist.navigation.AppjamtampMissionList +import org.sopt.official.feature.appjamtamp.ranking.AppjamtampRankingRoute +import org.sopt.official.feature.appjamtamp.ranking.navigation.AppjamtampRanking +import org.sopt.official.feature.appjamtamp.teammisisonlist.AppjamtampTeamMissionListRoute +import org.sopt.official.feature.appjamtamp.teammisisonlist.navigation.AppjamtampTeamMissionList + +@Serializable +data object AppjamtampNavGraph : MainTabRoute + +fun NavGraphBuilder.appjamtampNavGraph( + paddingValues: PaddingValues, + navController: NavController +) { + navigation( + startDestination = AppjamtampMissionList + ) { + composable { + AppjamtampMissionRoute( + navigateToMissionDetail = navController::navigateToMissionDetail, + navigateToRanking = navController::navigateToRanking + ) + } + + composable { + MissionDetailRoute( + navigateUp = navController::navigateUp, + ) + } + + composable { + AppjamtampRankingRoute( + navigateUp = navController::navigateUp, + navigateToTeamMissionList = navController::navigateToTeamMissionList + ) + } + + composable { backStackEntry -> + val args = backStackEntry.toRoute() + + AppjamtampTeamMissionListRoute( + teamNumber = args.teamNumber, + navigateUp = navController::navigateUp, + navigateToMissionDetail = navController::navigateToMissionDetail + ) + } + } +} diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/navigation/AppjamtampNavigation.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/navigation/AppjamtampNavigation.kt new file mode 100644 index 000000000..1c67ca7d9 --- /dev/null +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/navigation/AppjamtampNavigation.kt @@ -0,0 +1,44 @@ +package org.sopt.official.feature.appjamtamp.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavOptions +import org.sopt.official.feature.appjamtamp.missiondetail.navigation.AppjamtampMissionDetail +import org.sopt.official.feature.appjamtamp.missionlist.navigation.AppjamtampMissionList +import org.sopt.official.feature.appjamtamp.ranking.navigation.AppjamtampRanking +import org.sopt.official.feature.appjamtamp.teammisisonlist.navigation.AppjamtampTeamMissionList + +fun NavController.navigateToAppjamtamp(navOptions: NavOptions? = null) { + navigate(AppjamtampNavGraph, navOptions) +} + +internal fun NavController.navigateToMissionList(navOptions: NavOptions? = null) { + navigate(AppjamtampMissionList, navOptions) +} + +internal fun NavController.navigateToMissionDetail( + missionId: Int, + missionLevel: Int, + title: String, + ownerName: String?, + navOptions: NavOptions? = null +) { + navigate( + AppjamtampMissionDetail( + missionId = missionId, + missionLevel = missionLevel, + title = title, + ownerName = ownerName + ), navOptions + ) +} + +internal fun NavController.navigateToRanking(navOptions: NavOptions? = null) { + navigate(AppjamtampRanking, navOptions) +} + +internal fun NavController.navigateToTeamMissionList( + teamNumber: String, + navOptions: NavOptions? = null +) { + navigate(AppjamtampTeamMissionList(teamNumber), navOptions) +} diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/AppjamtampRankingScreen.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/AppjamtampRankingScreen.kt index 8a8c3bf83..6e9434d51 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/AppjamtampRankingScreen.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/AppjamtampRankingScreen.kt @@ -1,6 +1,7 @@ package org.sopt.official.feature.appjamtamp.ranking import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -31,7 +32,7 @@ import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -43,14 +44,16 @@ import org.sopt.official.feature.appjamtamp.component.BackButtonHeader import org.sopt.official.feature.appjamtamp.ranking.component.TodayScoreRaking import org.sopt.official.feature.appjamtamp.ranking.component.Top3RecentRankingMission import org.sopt.official.feature.appjamtamp.ranking.model.AppjamtampRankingState -import org.sopt.official.feature.appjamtamp.ranking.model.Top10MissionScoreListUiModel import org.sopt.official.feature.appjamtamp.ranking.model.Top3RecentRankingListUiModel import org.sopt.official.feature.appjamtamp.ranking.model.Top3RecentRankingUiModel +import org.sopt.official.feature.appjamtamp.ranking.model.TopMissionScoreListUiModel import org.sopt.official.feature.appjamtamp.ranking.model.TopMissionScoreUiModel @Composable internal fun AppjamtampRankingRoute( - viewModel:AppjamtampRankingViewModel= hiltViewModel() + navigateUp: () -> Unit, + navigateToTeamMissionList: (String) -> Unit, + viewModel: AppjamtampRankingViewModel = hiltViewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -58,16 +61,22 @@ internal fun AppjamtampRankingRoute( viewModel.getRankingData() } - when(state) { - is AppjamtampRankingState.Loading -> { LoadingIndicator() } + when (state) { + is AppjamtampRankingState.Loading -> { + LoadingIndicator() + } + is AppjamtampRankingState.Success -> { val top3RecentRankingList = (state as AppjamtampRankingState.Success).top3RecentRankingListUiModel.top3RecentRankingList - val top10MissionScoreList = (state as AppjamtampRankingState.Success).top10MissionScoreListUiModel.top10MissionScoreList + val top10MissionScoreList = (state as AppjamtampRankingState.Success).topMissionScoreListUiModel.top10MissionScoreList AppjamtampRankingScreen( top3RecentRankings = top3RecentRankingList, - top10MissionScores = top10MissionScoreList + top10MissionScores = top10MissionScoreList, + onBackButtonClick = navigateUp, + onTeamRankingClick = navigateToTeamMissionList ) } + is AppjamtampRankingState.Failure -> {} } } @@ -75,7 +84,9 @@ internal fun AppjamtampRankingRoute( @Composable internal fun AppjamtampRankingScreen( top3RecentRankings: ImmutableList, - top10MissionScores: ImmutableList + top10MissionScores: ImmutableList, + onBackButtonClick: () -> Unit, + onTeamRankingClick: (String) -> Unit ) { val scrollState = rememberScrollState() @@ -91,9 +102,7 @@ internal fun AppjamtampRankingScreen( topBar = { BackButtonHeader( title = "앱잼팀 랭킹", - onBackButtonClick = { - // TODO - 뒤로가기 (앱잼탬프 홈 - AppjamtampMissionScreen) - }, + onBackButtonClick = onBackButtonClick, modifier = Modifier .padding(vertical = 12.dp) .padding(start = 16.dp) @@ -149,7 +158,11 @@ internal fun AppjamtampRankingScreen( top3RecentRankings.forEach { top3RecentRanking -> Top3RecentRankingMission( top3RecentRanking = top3RecentRanking, - modifier = Modifier.width(topRankingItemWidth) + modifier = Modifier + .width(topRankingItemWidth) + .clickable { + onTeamRankingClick(top3RecentRanking.teamNumber) + } ) } } @@ -196,11 +209,9 @@ internal fun AppjamtampRankingScreen( ) { top10MissionScores.forEach { item -> TodayScoreRaking( - top10MissionScore = item, + topMissionScore = item, modifier = Modifier.weight(1f), - onTeamRankingClick = { - // Todo : 앱잼 팀 미션 화면으로 이동 (teamNumber 전달) - } + onTeamRankingClick = onTeamRankingClick ) } } @@ -250,24 +261,26 @@ private fun AppjamtampRankingScreenPreview() { ) ) - val mockTop10MissionScoreListUiModel = Top10MissionScoreListUiModel( + val mockTopMissionScoreListUiModel = TopMissionScoreListUiModel( top10MissionScoreList = persistentListOf( - TopMissionScoreUiModel(rank = 1, teamName = "보핏", todayPoints = 1200, totalPoints = 5000), - TopMissionScoreUiModel(rank = 2, teamName = "노바", todayPoints = 1100, totalPoints = 4800), - TopMissionScoreUiModel(rank = 3, teamName = "비트", todayPoints = 950, totalPoints = 4200), - TopMissionScoreUiModel(rank = 4, teamName = "하이링구얼", todayPoints = 800, totalPoints = 3900), - TopMissionScoreUiModel(rank = 5, teamName = "납작마켓", todayPoints = 750, totalPoints = 3500), - TopMissionScoreUiModel(rank = 6, teamName = "웹", todayPoints = 600, totalPoints = 3100), - TopMissionScoreUiModel(rank = 7, teamName = "안드로이드", todayPoints = 550, totalPoints = 2800), - TopMissionScoreUiModel(rank = 8, teamName = "iOS", todayPoints = 400, totalPoints = 2500), - TopMissionScoreUiModel(rank = 9, teamName = "디자인", todayPoints = 300, totalPoints = 2000), - TopMissionScoreUiModel(rank = 10, teamName = "기획", todayPoints = 100, totalPoints = 1500) + TopMissionScoreUiModel(rank = 1, teamName = "보핏", teamNumber = "FIRST", todayPoints = 1200, totalPoints = 5000), + TopMissionScoreUiModel(rank = 2, teamName = "노바", teamNumber = "FIRST", todayPoints = 1100, totalPoints = 4800), + TopMissionScoreUiModel(rank = 3, teamName = "비트", teamNumber = "FIRST", todayPoints = 950, totalPoints = 4200), + TopMissionScoreUiModel(rank = 4, teamName = "하이링구얼", teamNumber = "FIRST", todayPoints = 800, totalPoints = 3900), + TopMissionScoreUiModel(rank = 5, teamName = "납작마켓", teamNumber = "FIRST", todayPoints = 750, totalPoints = 3500), + TopMissionScoreUiModel(rank = 6, teamName = "웹", teamNumber = "FIRST", todayPoints = 600, totalPoints = 3100), + TopMissionScoreUiModel(rank = 7, teamName = "안드로이드", teamNumber = "FIRST", todayPoints = 550, totalPoints = 2800), + TopMissionScoreUiModel(rank = 8, teamName = "iOS", teamNumber = "FIRST", todayPoints = 400, totalPoints = 2500), + TopMissionScoreUiModel(rank = 9, teamName = "디자인", teamNumber = "FIRST", todayPoints = 300, totalPoints = 2000), + TopMissionScoreUiModel(rank = 10, teamName = "기획", teamNumber = "FIRST", todayPoints = 100, totalPoints = 1500) ) ) AppjamtampRankingScreen( top3RecentRankings = mockTop3RecentRankingListUiModel.top3RecentRankingList, - top10MissionScores = mockTop10MissionScoreListUiModel.top10MissionScoreList + top10MissionScores = mockTopMissionScoreListUiModel.top10MissionScoreList, + onBackButtonClick = {}, + onTeamRankingClick = { _ -> } ) } } diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/AppjamtampRankingViewModel.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/AppjamtampRankingViewModel.kt index 3251d3b99..0617ecad7 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/AppjamtampRankingViewModel.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/AppjamtampRankingViewModel.kt @@ -3,6 +3,7 @@ package org.sopt.official.feature.appjamtamp.ranking import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -12,7 +13,6 @@ import org.sopt.official.domain.appjamtamp.repository.AppjamtampRepository import org.sopt.official.feature.appjamtamp.ranking.model.AppjamtampRankingState import org.sopt.official.feature.appjamtamp.ranking.model.toUiModel import timber.log.Timber -import javax.inject.Inject @HiltViewModel internal class AppjamtampRankingViewModel @Inject constructor( @@ -35,7 +35,7 @@ internal class AppjamtampRankingViewModel @Inject constructor( if (top3Result.isSuccess && top10Result.isSuccess) { _state.value = AppjamtampRankingState.Success( top3RecentRankingListUiModel = top3Result.getOrThrow().toUiModel(), - top10MissionScoreListUiModel = top10Result.getOrThrow().toUiModel() + topMissionScoreListUiModel = top10Result.getOrThrow().toUiModel() ) } else { val error = top3Result.exceptionOrNull() ?: top10Result.exceptionOrNull() ?: Exception("Unknown error") diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/TodayScoreRaking.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/TodayScoreRaking.kt index 72bc763cb..9dda88e2f 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/TodayScoreRaking.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/TodayScoreRaking.kt @@ -28,24 +28,26 @@ import org.sopt.official.feature.appjamtamp.util.noRippleClickable @Composable internal fun TodayScoreRaking( - top10MissionScore: TopMissionScoreUiModel, + topMissionScore: TopMissionScoreUiModel, modifier: Modifier = Modifier, onTeamRankingClick: (teamNumber: String) -> Unit = {} ) { - val rankIconRes = when (top10MissionScore.rank) { + val rankIconRes = when (topMissionScore.rank) { 1 -> R.drawable.ic_rank_1 2 -> R.drawable.ic_rank_2 3 -> R.drawable.ic_rank_3 else -> R.drawable.ic_ranking_default } - val rankTextColor = if (top10MissionScore.rank <= 3) Color.White else SoptTheme.colors.onSurface100 + val rankTextColor = if (topMissionScore.rank <= 3) Color.White else SoptTheme.colors.onSurface100 Column( modifier = modifier .clip(shape = RoundedCornerShape(size = 10.dp)) .background(color = SoptTheme.colors.onSurface900) .padding(start = 12.dp, end = 16.dp, top = 12.dp, bottom = 8.dp) - .noRippleClickable { onTeamRankingClick } + .noRippleClickable { + onTeamRankingClick(topMissionScore.teamNumber) + } ) { Row( modifier = Modifier @@ -62,7 +64,7 @@ internal fun TodayScoreRaking( tint = Color.Unspecified ) Text( - text = top10MissionScore.rank.toString(), + text = topMissionScore.rank.toString(), style = SoptTheme.typography.heading16B, color = rankTextColor, modifier = Modifier.padding(vertical = 2.dp, horizontal = 9.dp) @@ -70,7 +72,7 @@ internal fun TodayScoreRaking( } Text( - text = top10MissionScore.teamName, + text = topMissionScore.teamName, color = White, style = SoptTheme.typography.heading16B, modifier = Modifier @@ -82,14 +84,14 @@ internal fun TodayScoreRaking( Spacer(modifier = Modifier.height(height = 14.dp)) Text( - text = "총 ${top10MissionScore.totalPoints}점", + text = "총 ${topMissionScore.totalPoints}점", color = SoptTheme.colors.onSurface300, style = SoptTheme.typography.title14SB, modifier = Modifier.align(Alignment.End) ) Text( - text = "+${top10MissionScore.todayPoints}점", + text = "+${topMissionScore.todayPoints}점", color = White, style = SoptTheme.typography.heading20B, modifier = Modifier.align(Alignment.End) @@ -104,10 +106,11 @@ private fun TodayScoreRakingPreview() { val mockTopMissionScoreUiModel = TopMissionScoreUiModel( rank = 2, teamName = "노바", + teamNumber = "FIRST", todayPoints = 1000, totalPoints = 3000 ) - TodayScoreRaking(top10MissionScore = mockTopMissionScoreUiModel) + TodayScoreRaking(topMissionScore = mockTopMissionScoreUiModel) } } diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/Top3RecentRankingMission.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/Top3RecentRankingMission.kt index d1365f15b..573a38c9b 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/Top3RecentRankingMission.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/component/Top3RecentRankingMission.kt @@ -27,17 +27,17 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import java.util.concurrent.TimeUnit import org.sopt.official.designsystem.GrayAlpha100 import org.sopt.official.designsystem.SoptTheme import org.sopt.official.designsystem.White import org.sopt.official.designsystem.component.UrlImage import org.sopt.official.feature.appjamtamp.R import org.sopt.official.feature.appjamtamp.ranking.model.Top3RecentRankingUiModel -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone -import java.util.concurrent.TimeUnit @Composable internal fun Top3RecentRankingMission( @@ -128,35 +128,29 @@ internal fun Top3RecentRankingMission( } } -/* 업로드 시간 변경 함수 -서버 응답 형식 : 2025-10-31T00:00:56 -업로드 시간: 상대시간 노출 -10분 미만 => 방금 전 -11분 전 ~ 59분 전 => 그대로 표기 -1시간 ~ 24시간 전 => 그대로 표기 -25시간 이후 => 1일 전, 2일 전 ... - */ private fun String?.toRelativeTime(): String { if (this.isNullOrBlank()) return "" val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.KOREA) dateFormat.timeZone = TimeZone.getTimeZone("Asia/Seoul") - val date = dateFormat .parse(this) ?: return "" + val date = dateFormat.parse(this) ?: return "" val currentDate = Date() val diffMillis = currentDate.time - date.time - if (diffMillis < 0) return "방금 전" + if (diffMillis < 0) return "1분 전" val minutes = TimeUnit.MILLISECONDS.toMinutes(diffMillis) val hours = TimeUnit.MILLISECONDS.toHours(diffMillis) - val days = TimeUnit.MILLISECONDS.toDays(diffMillis) return when { - minutes < 10 -> "방금 전" - minutes in 10..59 -> "${minutes}분 전" - hours in 1..23 -> "${hours}시간 전" - else -> "${days}일 전" + minutes == 0L -> "1분 전" + minutes in 1..59 -> "${minutes}분 전" + hours in 1..24 -> "${hours}시간 전" + else -> { + val days = TimeUnit.MILLISECONDS.toDays(diffMillis) + "${days}일 전" + } } } diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/model/AppjamtampState.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/model/AppjamtampState.kt index 337b2e65e..9895ef8a5 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/model/AppjamtampState.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/model/AppjamtampState.kt @@ -5,7 +5,7 @@ sealed class AppjamtampRankingState { data class Success( val top3RecentRankingListUiModel: Top3RecentRankingListUiModel, - val top10MissionScoreListUiModel: Top10MissionScoreListUiModel + val topMissionScoreListUiModel: TopMissionScoreListUiModel ) : AppjamtampRankingState() data class Failure(val error: Throwable) : AppjamtampRankingState() diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/model/TopMissionScoreUiModel.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/model/TopMissionScoreUiModel.kt index d0d2dd927..a298fa4a0 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/model/TopMissionScoreUiModel.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/model/TopMissionScoreUiModel.kt @@ -4,18 +4,19 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import org.sopt.official.domain.appjamtamp.entity.AppjamtampMissionScore -data class Top10MissionScoreListUiModel( +data class TopMissionScoreListUiModel( val top10MissionScoreList: ImmutableList ) -internal fun List.toUiModel(): Top10MissionScoreListUiModel = - Top10MissionScoreListUiModel( +internal fun List.toUiModel(): TopMissionScoreListUiModel = + TopMissionScoreListUiModel( top10MissionScoreList = this.map { it.toUiModel() }.toImmutableList() ) data class TopMissionScoreUiModel( val rank: Int, val teamName: String, + val teamNumber: String, val todayPoints: Int, val totalPoints: Int ) @@ -24,6 +25,7 @@ internal fun AppjamtampMissionScore.toUiModel(): TopMissionScoreUiModel = TopMissionScoreUiModel( rank = this.rank, teamName = this.teamName, + teamNumber = this.teamNumber, todayPoints = this.todayPoints, totalPoints = this.totalPoints ) diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/navigation/AppjamtampRanking.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/navigation/AppjamtampRanking.kt new file mode 100644 index 000000000..62881421b --- /dev/null +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/ranking/navigation/AppjamtampRanking.kt @@ -0,0 +1,7 @@ +package org.sopt.official.feature.appjamtamp.ranking.navigation + +import kotlinx.serialization.Serializable +import org.sopt.official.core.navigation.Route + +@Serializable +internal data object AppjamtampRanking : Route diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/AppjamtampTeamMissionListScreen.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/AppjamtampTeamMissionListScreen.kt index bdeebae2d..6e0cd56f0 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/AppjamtampTeamMissionListScreen.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/AppjamtampTeamMissionListScreen.kt @@ -32,24 +32,37 @@ import org.sopt.official.feature.appjamtamp.teammisisonlist.model.AppjamtampMiss @Composable internal fun AppjamtampTeamMissionListRoute( + teamNumber: String, + navigateUp: () -> Unit, + navigateToMissionDetail: (missionId: Int, missionLevel: Int, title: String, ownerName: String?) -> Unit, viewModel: AppjamtampTeamMissionListViewModel = hiltViewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() LaunchedEffect(Unit) { viewModel.fetchAppjamMissions( - teamNumber = "FIRST" // TODO - 이전 화면에서 네비게이션으로 teamNumber 전달 받아 넣기 + teamNumber = teamNumber ) } - when(state) { + when (state) { is AppjamtampMissionListState.Loading -> {} is AppjamtampMissionListState.Success -> { AppjamtampTeamMissionListScreen( teamName = (state as AppjamtampMissionListState.Success).teamName, - teamMissionList = (state as AppjamtampMissionListState.Success).teamMissionList.missionList + teamMissionList = (state as AppjamtampMissionListState.Success).teamMissionList.missionList, + onBackButtonClick = navigateUp, + onMissionItemClick = { mission -> + navigateToMissionDetail( + mission.id, + mission.level.value, + mission.title, + mission.ownerName + ) + } ) } + is AppjamtampMissionListState.Failure -> {} } } @@ -57,7 +70,9 @@ internal fun AppjamtampTeamMissionListRoute( @Composable internal fun AppjamtampTeamMissionListScreen( teamName: String, - teamMissionList: ImmutableList + teamMissionList: ImmutableList, + onBackButtonClick: () -> Unit, + onMissionItemClick: (AppjamtampMissionUiModel) -> Unit ) { Scaffold( modifier = Modifier @@ -66,9 +81,7 @@ internal fun AppjamtampTeamMissionListScreen( topBar = { BackButtonHeader( title = teamName, - onBackButtonClick = { - // Todo : 앱잼 팀 랭킹 화면으로 이동 (뒤로가기) - }, + onBackButtonClick = onBackButtonClick, modifier = Modifier .padding(vertical = 12.dp) .padding(start = 16.dp) @@ -91,9 +104,7 @@ internal fun AppjamtampTeamMissionListScreen( MissionsGridComponent( missionList = teamMissionList, - onMissionItemClick = { item -> - // Todo : 미션 상세화면으로 이동 - } + onMissionItemClick = onMissionItemClick ) } } @@ -162,6 +173,11 @@ private fun AppjamtampTeamMissionListScreenPreview() { ) SoptTheme { - AppjamtampTeamMissionListScreen(teamName = "하이링구얼", teamMissionList = mockMissionList) + AppjamtampTeamMissionListScreen( + teamName = "하이링구얼", + teamMissionList = mockMissionList, + onBackButtonClick = {}, + onMissionItemClick = { _ -> } + ) } } diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/AppjamtampTeamMissionListViewModel.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/AppjamtampTeamMissionListViewModel.kt index db3144c13..5007084ea 100644 --- a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/AppjamtampTeamMissionListViewModel.kt +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/AppjamtampTeamMissionListViewModel.kt @@ -3,15 +3,19 @@ package org.sopt.official.feature.appjamtamp.teammisisonlist import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.sopt.official.domain.appjamtamp.entity.AppjamtampMissionListEntity +import org.sopt.official.domain.appjamtamp.entity.MissionLevel import org.sopt.official.domain.appjamtamp.repository.AppjamtampRepository -import org.sopt.official.feature.appjamtamp.missionlist.model.toUiModel +import org.sopt.official.feature.appjamtamp.missionlist.model.AppjamtampMissionListUiModel +import org.sopt.official.feature.appjamtamp.missionlist.model.AppjamtampMissionUiModel import org.sopt.official.feature.appjamtamp.teammisisonlist.model.AppjamtampMissionListState import timber.log.Timber -import javax.inject.Inject @HiltViewModel internal class AppjamtampTeamMissionListViewModel @Inject constructor( @@ -25,15 +29,14 @@ internal class AppjamtampTeamMissionListViewModel @Inject constructor( val teamName: StateFlow = _teamName.asStateFlow() fun fetchAppjamMissions( - teamNumber: String, - isCompleted: Boolean? = null + teamNumber: String ) { viewModelScope.launch { _state.value = AppjamtampMissionListState.Loading appjamtampRepository.getAppjamtampMissions( teamNumber = teamNumber, - isCompleted = isCompleted + isCompleted = true ).onSuccess { missions -> _state.value = AppjamtampMissionListState.Success( teamName = missions.teamName, @@ -42,4 +45,19 @@ internal class AppjamtampTeamMissionListViewModel @Inject constructor( }.onFailure(Timber::e) } } + + private fun AppjamtampMissionListEntity.toUiModel() = AppjamtampMissionListUiModel( + teamNumber = teamNumber, + teamName = teamName, + missionList = missions.map { + AppjamtampMissionUiModel( + id = it.id, + title = "${it.title} (${it.ownerName}이 작성한 미션)", + ownerName = it.ownerName, + level = MissionLevel.of(it.level), + profileImage = it.profileImage, + isCompleted = it.isCompleted, + ) + }.toImmutableList() + ) } diff --git a/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/navigation/AppjamtampTeamMissionList.kt b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/navigation/AppjamtampTeamMissionList.kt new file mode 100644 index 000000000..e5069a835 --- /dev/null +++ b/feature/appjamtamp/src/main/java/org/sopt/official/feature/appjamtamp/teammisisonlist/navigation/AppjamtampTeamMissionList.kt @@ -0,0 +1,9 @@ +package org.sopt.official.feature.appjamtamp.teammisisonlist.navigation + +import kotlinx.serialization.Serializable +import org.sopt.official.core.navigation.Route + +@Serializable +internal data class AppjamtampTeamMissionList( + val teamNumber: String +) : Route diff --git a/feature/appjamtamp/src/main/res/drawable/ic_back.xml b/feature/appjamtamp/src/main/res/drawable/ic_back.xml deleted file mode 100644 index 656d62307..000000000 --- a/feature/appjamtamp/src/main/res/drawable/ic_back.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - diff --git a/feature/main/src/main/java/org/sopt/official/feature/main/MainNavigator.kt b/feature/main/src/main/java/org/sopt/official/feature/main/MainNavigator.kt index 83817b918..9154b0619 100644 --- a/feature/main/src/main/java/org/sopt/official/feature/main/MainNavigator.kt +++ b/feature/main/src/main/java/org/sopt/official/feature/main/MainNavigator.kt @@ -34,13 +34,13 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import org.sopt.official.core.navigation.Route +import org.sopt.official.feature.appjamtamp.navigation.navigateToAppjamtamp import org.sopt.official.feature.home.navigation.Home import org.sopt.official.feature.home.navigation.navigateToHome import org.sopt.official.feature.poke.navigation.PokeGraph import org.sopt.official.feature.poke.navigation.navigateToPokeEntry import org.sopt.official.feature.soptlog.navigation.navigateToSoptLog import org.sopt.official.model.UserStatus -import org.sopt.official.stamp.feature.navigation.SoptampGraph import org.sopt.official.stamp.feature.navigation.navigateToSoptamp class MainNavigator( @@ -90,6 +90,10 @@ class MainNavigator( navOptions = navOptions ) + MainTab.Appjamtamp -> navController.navigateToAppjamtamp( + navOptions = navOptions + ) + MainTab.Poke -> { navController.navigateToPokeEntry( navOptions = navOptions @@ -126,6 +130,15 @@ class MainNavigator( ) } + MainTab.Appjamtamp -> { + navController.navigateToAppjamtamp( + navOptions = navOptions { + popUpTo { inclusive = true } + launchSingleTop = true + } + ) + } + MainTab.Poke -> { // PokeGraph가 있다고 가정 navController.navigateToPokeEntry( navOptions = navOptions { diff --git a/feature/main/src/main/java/org/sopt/official/feature/main/MainScreen.kt b/feature/main/src/main/java/org/sopt/official/feature/main/MainScreen.kt index ea9cef6e7..9434c874d 100644 --- a/feature/main/src/main/java/org/sopt/official/feature/main/MainScreen.kt +++ b/feature/main/src/main/java/org/sopt/official/feature/main/MainScreen.kt @@ -85,6 +85,7 @@ import org.sopt.official.common.navigator.DeepLinkType import org.sopt.official.common.navigator.NavigatorProvider import org.sopt.official.common.view.toast import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.feature.appjamtamp.navigation.appjamtampNavGraph import org.sopt.official.feature.home.navigation.HomeNavigation.HomeAppServicesNavigation import org.sopt.official.feature.home.navigation.HomeNavigation.HomeDashboardNavigation import org.sopt.official.feature.home.navigation.HomeNavigation.HomeShortcutNavigation @@ -141,6 +142,13 @@ fun MainScreen( hasSoptampFlag || hasMissionArgs } + val shouldNavigateToAppjamtamp = { + val intent = intentState + val hasAppjamtampFlag = intent?.getBooleanExtra("isAppjamtampDeepLink", false) == true + val hasMissionArgs = intent?.hasExtra("appjamtampArgs") == true + hasAppjamtampFlag || hasMissionArgs + } + // soptLog 검사 로직 val shouldNavigateToSoptLog = remember(intentState) { val intent = intentState @@ -164,13 +172,25 @@ fun MainScreen( intent?.hasExtra("friendType") == true } - LaunchedEffect(shouldNavigateToSoptamp, shouldNavigateToPoke, shouldNavigateToPokeNotification, shouldNavigatePokeFriendList, shouldNavigateToSoptLog) { + LaunchedEffect( + shouldNavigateToSoptamp, + shouldNavigateToAppjamtamp, + shouldNavigateToPoke, + shouldNavigateToPokeNotification, + shouldNavigatePokeFriendList, + shouldNavigateToSoptLog + ) { if (shouldNavigateToSoptamp()) { navigator.navigateAndClear(MainTab.Soptamp, userStatus) activity?.intent?.putExtra("isSoptampDeepLink", false) } - if(shouldNavigateToSoptLog) { + if (shouldNavigateToAppjamtamp()) { + navigator.navigateAndClear(MainTab.Appjamtamp, userStatus) + activity?.intent?.putExtra("isSoptampDeepLink", false) + } + + if (shouldNavigateToSoptLog) { navigator.navigate(MainTab.SoptLog, userStatus) activity?.intent?.putExtra("isSoptLogDeepLink", false) } @@ -256,6 +276,10 @@ fun MainScreen( navigator.navigate(MainTab.Soptamp, userStatus) } + DeepLinkType.APPJAMTAMP -> { + navigator.navigate(MainTab.Appjamtamp, userStatus) + } + else -> { context.startActivity(deepLinkType.getIntent(context, userStatus, url)) } @@ -290,6 +314,11 @@ fun MainScreen( currentIntent = intentState ) + appjamtampNavGraph( + paddingValues = innerPadding, + navController = navigator.navController + ) + pokeNavGraph( navController = navigator.navController, paddingValues = innerPadding, @@ -359,7 +388,7 @@ fun MainScreen( } SlideUpDownWithFadeAnimatedVisibility( - visible = navigator.currentTab != MainTab.Soptamp && navigator.currentTab != MainTab.Poke, + visible = navigator.currentTab == MainTab.Home || navigator.currentTab == MainTab.SoptLog ) { MainFloatingButton( paddingValues = innerPadding @@ -369,7 +398,7 @@ fun MainScreen( ) if (isOpenDialog) { - UnauthenticatedDDialog( + UnauthenticatedDialog( onDismissRequest = { isOpenDialog = false }, onLogin = { context.startActivity( @@ -382,7 +411,7 @@ fun MainScreen( } @Composable -fun UnauthenticatedDDialog( +fun UnauthenticatedDialog( onDismissRequest: () -> Unit, onLogin: () -> Unit, ) { diff --git a/feature/main/src/main/java/org/sopt/official/feature/main/MainTab.kt b/feature/main/src/main/java/org/sopt/official/feature/main/MainTab.kt index 9273a71fc..482c470fe 100644 --- a/feature/main/src/main/java/org/sopt/official/feature/main/MainTab.kt +++ b/feature/main/src/main/java/org/sopt/official/feature/main/MainTab.kt @@ -52,6 +52,14 @@ enum class MainTab( deeplink = "soptamp" ), + Appjamtamp( + icon = R.drawable.ic_main_soptamp, + contentDescription = "솝탬프", + route = org.sopt.official.feature.appjamtamp.navigation.AppjamtampNavGraph, + loggingName = null, + deeplink = "appjamtamp" + ), + Poke( icon = R.drawable.ic_main_poke, contentDescription = "콕찌르기",