From 5baa065d524941a286eccdde5bf364bc328294ee Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Thu, 8 Oct 2026 21:22:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(challenge):=20=EB=B0=A9=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=20=EC=A0=95=EB=B3=B4=EC=97=90=20=EB=82=B4=20=EB=A3=A8?= =?UTF-8?q?=ED=8B=B4=20=EC=A7=84=ED=96=89=EB=A5=A0=EC=9D=84=20=EB=B3=B4?= =?UTF-8?q?=EC=97=AC=EC=A4=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 방 응답 routineProgress 를 ChallengeRoom.routineProgress 로 옮긴다(% → 0~1) - 진행 정보 카드에 「내 진행률 N% · 성공일/목표일」을 표시하고, 목표일이 0이면 분모를 숨긴다 - 방 평균은 기존 「방 성공률」과 같은 값이라 그 줄을 그대로 쓰고, 솔로 방에서는 숨긴다 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ruleup/challenge/data/dto/RoomResponse.kt | 22 ++++++++++++++++ .../challenge/data/dto/RoomMappingTest.kt | 25 +++++++++++++++++++ .../ruleup/challenge/domain/entity/Room.kt | 13 ++++++++++ .../detail/component/RoomInfoTab.kt | 25 +++++++++++++++---- 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/challenge/data/src/main/kotlin/com/ruleup/challenge/data/dto/RoomResponse.kt b/challenge/data/src/main/kotlin/com/ruleup/challenge/data/dto/RoomResponse.kt index 8d7256fb9..0105a112d 100644 --- a/challenge/data/src/main/kotlin/com/ruleup/challenge/data/dto/RoomResponse.kt +++ b/challenge/data/src/main/kotlin/com/ruleup/challenge/data/dto/RoomResponse.kt @@ -5,6 +5,7 @@ import com.ruleup.challenge.domain.entity.MemberRole import com.ruleup.challenge.domain.entity.OwnerType import com.ruleup.challenge.domain.entity.RoomSummary import com.ruleup.challenge.domain.entity.RoomTopRanker +import com.ruleup.challenge.domain.entity.RoutineProgress import com.ruleup.challenge.domain.entity.TodayVerificationStatus import com.ruleup.domain.entity.user.User import com.ruleup.domain.entity.user.UserRelationship @@ -89,6 +90,8 @@ data class RoomResponse( val topRanking: List? = null, @SerialName("myTodayStatus") val myTodayStatus: String? = null, + @SerialName("routineProgress") + val routineProgress: RoomRoutineProgressResponse? = null, ) internal fun RoomResponse.toDomain(): ChallengeRoom = @@ -108,4 +111,23 @@ internal fun RoomResponse.toDomain(): ChallengeRoom = topRanking = topRanking.orEmpty().mapNotNull { it.toDomain() }, // 미지 값은 null myTodayStatus = TodayVerificationStatus.fromValue(myTodayStatus), + routineProgress = routineProgress?.toDomain(), + ) + +/** 루틴 진행률. 비율은 %(0~100) 로 온다. roomAverageProgressRate 는 summary.roomSuccessRate 와 같은 값이라 읽지 않는다. */ +@Serializable +data class RoomRoutineProgressResponse( + @SerialName("myProgressRate") + val myProgressRate: Double? = null, + @SerialName("mySuccessDays") + val mySuccessDays: Int? = null, + @SerialName("myTargetDays") + val myTargetDays: Int? = null, +) + +internal fun RoomRoutineProgressResponse.toDomain(): RoutineProgress = + RoutineProgress( + myProgressRate = (myProgressRate ?: 0.0) / 100, + mySuccessDays = mySuccessDays ?: 0, + myTargetDays = myTargetDays ?: 0, ) diff --git a/challenge/data/src/test/kotlin/com/ruleup/challenge/data/dto/RoomMappingTest.kt b/challenge/data/src/test/kotlin/com/ruleup/challenge/data/dto/RoomMappingTest.kt index c8e56c950..d5abcf74b 100644 --- a/challenge/data/src/test/kotlin/com/ruleup/challenge/data/dto/RoomMappingTest.kt +++ b/challenge/data/src/test/kotlin/com/ruleup/challenge/data/dto/RoomMappingTest.kt @@ -3,6 +3,7 @@ package com.ruleup.challenge.data.dto import com.ruleup.challenge.domain.entity.MemberRole import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue @@ -47,4 +48,28 @@ class RoomMappingTest { assertTrue(room.topRanking.isEmpty()) } + + @Test + fun `루틴 진행률은 퍼센트를 0에서 1 사이 비율로 옮긴다`() { + val progress = + RoomResponse(routineProgress = RoomRoutineProgressResponse(myProgressRate = 72.5, mySuccessDays = 18, myTargetDays = 25)) + .toDomain() + .routineProgress!! + + assertEquals(0.725, progress.myProgressRate, 1e-9) + assertEquals(18, progress.mySuccessDays) + assertEquals(25, progress.myTargetDays) + } + + @Test + fun `목표일이 아직 계산 전이면 분모가 없는 것으로 본다`() { + val progress = RoomResponse(routineProgress = RoomRoutineProgressResponse(myTargetDays = 0)).toDomain().routineProgress!! + + assertFalse(progress.hasTargetDays) + } + + @Test + fun `루틴 진행률이 없으면 비워 둔다`() { + assertNull(RoomResponse().toDomain().routineProgress) + } } diff --git a/challenge/domain/src/main/kotlin/com/ruleup/challenge/domain/entity/Room.kt b/challenge/domain/src/main/kotlin/com/ruleup/challenge/domain/entity/Room.kt index b6fb532d2..2bd230883 100644 --- a/challenge/domain/src/main/kotlin/com/ruleup/challenge/domain/entity/Room.kt +++ b/challenge/domain/src/main/kotlin/com/ruleup/challenge/domain/entity/Room.kt @@ -63,4 +63,17 @@ data class ChallengeRoom( // 상위 3. val topRanking: List, val myTodayStatus: TodayVerificationStatus?, + // 응답에 없으면 null + val routineProgress: RoutineProgress? = null, ) + +/** 챌린지 전체 기간 기준 내 루틴 진행률. 방 평균은 [RoomSummary.roomSuccessRate] 와 같은 값이라 따로 두지 않는다. */ +data class RoutineProgress( + // 0~1 + val myProgressRate: Double, + val mySuccessDays: Int, + // 첫 인증 전에는 아직 계산 전이라 0 이다 + val myTargetDays: Int, +) { + val hasTargetDays: Boolean get() = myTargetDays > 0 +} diff --git a/challenge/presentation/src/main/kotlin/com/ruleup/challenge/presentation/detail/component/RoomInfoTab.kt b/challenge/presentation/src/main/kotlin/com/ruleup/challenge/presentation/detail/component/RoomInfoTab.kt index a9e003fda..a51aafb0e 100644 --- a/challenge/presentation/src/main/kotlin/com/ruleup/challenge/presentation/detail/component/RoomInfoTab.kt +++ b/challenge/presentation/src/main/kotlin/com/ruleup/challenge/presentation/detail/component/RoomInfoTab.kt @@ -464,11 +464,26 @@ internal fun ProgressInfoCard( owner?.let { append(" · $it") } }, ) - InfoLine( - label = "방 성공률", - // 판정 이력이 없으면 null 이다. - value = room.summary.roomSuccessRate?.let { "${it.toPercentText()}%" } ?: "아직 집계 전", - ) + room.routineProgress?.let { progress -> + InfoLine( + label = "내 진행률", + value = + buildString { + append("${progress.myProgressRate.toPercentText()}% · ${progress.mySuccessDays}") + // 목표일이 아직 계산 전(0)이면 분모를 숨긴다 + if (progress.hasTargetDays) append("/${progress.myTargetDays}") + append("일") + }, + ) + } + // 솔로는 방 평균이 곧 내 값이라 내 진행률만 둔다 + if (detail.mode.isGroup) { + InfoLine( + label = "방 성공률", + // 판정 이력이 없으면 null 이다. + value = room.summary.roomSuccessRate?.let { "${it.toPercentText()}%" } ?: "아직 집계 전", + ) + } } }