diff --git a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/WeeklyReportMapper.kt b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/WeeklyReportMapper.kt index 3174f02b22..95841dc007 100644 --- a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/WeeklyReportMapper.kt +++ b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/WeeklyReportMapper.kt @@ -22,6 +22,14 @@ fun WeeklyReportDto.toDomain(): WeeklyReport = /** `week[].type` 중 일기를 뜻하는 값. 나머지(`DAILY_QUESTION`·`DEEP_THOUGHT`·미래 종류)는 일기가 아니다. */ private const val WEEK_RECORD_TYPE_DIARY = "DIARY" +/** + * 기록일수에서 제외할 종류. + * + * 깊은 생각은 기획에서 제거됐다. 서버는 `week[]` 에 계속 실어 보내지만 앱은 이 기능을 + * 없는 것으로 다루므로 기록일수에도 세지 않는다 (#590). + */ +private val WEEK_RECORD_TYPES_NOT_COUNTED = setOf("DEEP_THOUGHT") + /** * 와이어의 `type` 문자열을 도메인 의미(`isDiary`)로 접는다. * @@ -33,6 +41,7 @@ fun WeeklyReportDayDto.toDomain(): WeeklyReportDay = diaryId = diaryId, day = day, isDiary = type.equals(WEEK_RECORD_TYPE_DIARY, ignoreCase = true), + countsAsRecord = WEEK_RECORD_TYPES_NOT_COUNTED.none { it.equals(type, ignoreCase = true) }, emotion = emotion?.toDomain(), ) diff --git a/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/WeeklyReportModels.kt b/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/WeeklyReportModels.kt index 25eee759ef..6155d128a7 100644 --- a/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/WeeklyReportModels.kt +++ b/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/WeeklyReportModels.kt @@ -13,6 +13,13 @@ data class WeeklyReportDay( val diaryId: Long, val day: Int, val isDiary: Boolean, + /** + * 기록일수에 세는 종류인지 (#590). + * + * `week[]` 는 일기 외의 종류도 싣는다. 데일리질문은 세지만 **깊은 생각은 기획에서 + * 제거된 기능이라 세지 않는다** — 서버가 계속 내려줘도 무시한다. + */ + val countsAsRecord: Boolean, val emotion: TodayMood?, ) diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportRecordedDays.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportRecordedDays.kt index e0c628e9eb..6e8e050420 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportRecordedDays.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportRecordedDays.kt @@ -31,6 +31,8 @@ internal fun resolveDateInWeekOrNull( */ internal data class DailyRecordSummary( val isDiary: Boolean, + /** 그 날짜에 **기록일수로 셀 수 있는** 기록이 하나라도 있었는지 (#590). */ + val countsAsRecord: Boolean, val emotion: TodayMood?, ) @@ -56,6 +58,7 @@ internal fun aggregateWeekRecordsByDate( .mapValues { (_, days) -> DailyRecordSummary( isDiary = days.any { it.isDiary }, + countsAsRecord = days.any { it.countsAsRecord }, emotion = days .filter { it.emotion != null } @@ -65,10 +68,19 @@ internal fun aggregateWeekRecordsByDate( } /** - * 기록일수 = 일기가 있는 날 + 주간 범위 내 데일리질문 답변 날짜의 합집합(중복 제거). + * 기록일수 = `week[]` 에 기록이 있는 날 + 주간 범위 내 데일리질문 답변 날짜의 합집합(중복 제거). * * 두 출처를 모두 `LocalDate` 로 복원한 뒤 합쳐야 같은 날의 일기·데일리질문이 * `distinct()` 로 1일로 접힌다. + * + * **일기만 세지 않는다.** 종전에는 `isDiary` 인 날만 셌는데, `week[]` 는 일기 외에 + * `DAILY_QUESTION` 원소도 싣는다 (실서버 대조 2026-08-23, #590). 문구가 세는 것은 + * "일기를 쓴 날" 이 아니라 "마음을 기록한 날" 이다. + * + * 다만 **깊은 생각은 제외한다** — 기획에서 제거된 기능이라 서버가 계속 내려줘도 앱은 + * 없는 것으로 다룬다. 그 판단은 매퍼가 [WeeklyReportDay.countsAsRecord] 로 접어 둔다. + * + * (`isDiary` 는 계속 달력 점 표시에만 쓴다 — [aggregateWeekRecordsByDate].) */ internal fun countRecordedDays( monday: LocalDate, @@ -76,10 +88,10 @@ internal fun countRecordedDays( dailyQuestionDates: List, ): Int { val sunday = monday.plusDays(WEEK_LENGTH - 1L) - val diaryDates = + val recordDates = aggregateWeekRecordsByDate(monday, week) - .filterValues { it.isDiary } + .filterValues { it.countsAsRecord } .keys val questionDatesInWeek = dailyQuestionDates.filter { it in monday..sunday } - return (diaryDates + questionDatesInWeek).distinct().size + return (recordDates + questionDatesInWeek).distinct().size } diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportRecordedDaysTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportRecordedDaysTest.kt index 902634d84e..7100c5ce37 100644 --- a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportRecordedDaysTest.kt +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportRecordedDaysTest.kt @@ -90,7 +90,8 @@ class WeeklyReportRecordedDaysTest { } @Test - fun `isDiary 가 false 인 원소는 일기 기록일로 세지 않는다`() { + fun `일기가 아닌 기록도 기록일로 센다`() { + // #590 회귀 — week[] 는 일기 외 종류도 싣는다. isDiary 로 거르면 그 날이 사라진다. val monday = LocalDate.of(2026, 7, 27) val recordedDays = @@ -100,9 +101,83 @@ class WeeklyReportRecordedDaysTest { dailyQuestionDates = emptyList(), ) + assertEquals(1, recordedDays) + } + + @Test + fun `깊은 생각만 있는 날은 세지 않는다`() { + // 기획에서 제거된 기능이라 서버가 week[] 에 계속 실어 보내도 앱은 없는 것으로 다룬다. + val monday = LocalDate.of(2026, 7, 27) + + val recordedDays = + countRecordedDays( + monday = monday, + week = listOf(diaryDay(day = 28, isDiary = false, countsAsRecord = false)), + dailyQuestionDates = emptyList(), + ) + assertEquals(0, recordedDays) } + @Test + fun `같은 날에 깊은 생각과 일기가 함께 있으면 센다`() { + // 제외 대상은 "깊은 생각뿐인 날" 이지 그 날짜 자체가 아니다. + val monday = LocalDate.of(2026, 7, 27) + + val recordedDays = + countRecordedDays( + monday = monday, + week = + listOf( + diaryDay(day = 28, isDiary = false, countsAsRecord = false), + diaryDay(day = 28), + ), + dailyQuestionDates = emptyList(), + ) + + assertEquals(1, recordedDays) + } + + @Test + fun `일기가 아닌 기록도 종류를 가리지 않고 날짜 단위로 센다`() { + // 문구가 세는 것은 "일기를 쓴 날" 이 아니라 "마음을 기록한 날" 이다. + val monday = LocalDate.of(2026, 7, 27) + + val recordedDays = + countRecordedDays( + monday = monday, + week = listOf(diaryDay(day = 28, isDiary = false), diaryDay(day = 30)), + dailyQuestionDates = emptyList(), + ) + + assertEquals(2, recordedDays) + } + + @Test + fun `week 의 비일기 기록과 같은 날 데일리질문은 1일로 접힌다`() { + // 두 출처가 같은 날을 가리키면 중복 제거된다 — 종류를 안 가려도 이중 계산은 없다. + val monday = LocalDate.of(2026, 7, 27) + + val recordedDays = + countRecordedDays( + monday = monday, + week = listOf(diaryDay(day = 28, isDiary = false)), + dailyQuestionDates = listOf(LocalDate.of(2026, 7, 28)), + ) + + assertEquals(1, recordedDays) + } + + @Test + fun `종류를 가리지 않아도 달력 점은 여전히 일기만 찍는다`() { + // isDiary 는 집계에서 빠졌을 뿐 표시 규칙에는 그대로 남는다. + val monday = LocalDate.of(2026, 7, 27) + + val byDate = aggregateWeekRecordsByDate(monday, listOf(diaryDay(day = 28, isDiary = false))) + + assertEquals(false, byDate.getValue(LocalDate.of(2026, 7, 28)).isDiary) + } + @Test fun `빈 week 응답은 데일리질문만으로 집계된다`() { val monday = LocalDate.of(2026, 7, 27) @@ -135,8 +210,8 @@ class WeeklyReportRecordedDaysTest { val monday = LocalDate.of(2026, 7, 27) val week = listOf( - WeeklyReportDay(diaryId = 1, day = 28, isDiary = false, emotion = null), - WeeklyReportDay(diaryId = 2, day = 28, isDiary = true, emotion = TodayMood.SAD), + WeeklyReportDay(diaryId = 1, day = 28, isDiary = false, countsAsRecord = true, emotion = null), + WeeklyReportDay(diaryId = 2, day = 28, isDiary = true, countsAsRecord = true, emotion = TodayMood.SAD), ) val byDate = aggregateWeekRecordsByDate(monday, week) @@ -152,8 +227,8 @@ class WeeklyReportRecordedDaysTest { // 명세에 week[] 순서 의미도 같은 날짜 복수 원소의 병합 규칙도 없다 (Afternote-BE#131). // 순서에 의존하면 서버가 정렬만 바꿔도 화면 이모지가 바뀐다 — diaryId 최대값으로 고정한다. val monday = LocalDate.of(2026, 7, 27) - val older = WeeklyReportDay(diaryId = 1, day = 28, isDiary = true, emotion = TodayMood.HAPPY) - val newer = WeeklyReportDay(diaryId = 2, day = 28, isDiary = true, emotion = TodayMood.SAD) + val older = WeeklyReportDay(diaryId = 1, day = 28, isDiary = true, countsAsRecord = true, emotion = TodayMood.HAPPY) + val newer = WeeklyReportDay(diaryId = 2, day = 28, isDiary = true, countsAsRecord = true, emotion = TodayMood.SAD) val ascending = aggregateWeekRecordsByDate(monday, listOf(older, newer)) val descending = aggregateWeekRecordsByDate(monday, listOf(newer, older)) @@ -167,8 +242,8 @@ class WeeklyReportRecordedDaysTest { val monday = LocalDate.of(2026, 7, 27) val week = listOf( - WeeklyReportDay(diaryId = 9, day = 28, isDiary = true, emotion = null), - WeeklyReportDay(diaryId = 1, day = 28, isDiary = false, emotion = TodayMood.HAPPY), + WeeklyReportDay(diaryId = 9, day = 28, isDiary = true, countsAsRecord = true, emotion = null), + WeeklyReportDay(diaryId = 1, day = 28, isDiary = false, countsAsRecord = true, emotion = TodayMood.HAPPY), ) val byDate = aggregateWeekRecordsByDate(monday, week) @@ -188,11 +263,13 @@ class WeeklyReportRecordedDaysTest { private fun diaryDay( day: Int, isDiary: Boolean = true, + countsAsRecord: Boolean = true, ): WeeklyReportDay = WeeklyReportDay( diaryId = day.toLong(), day = day, isDiary = isDiary, + countsAsRecord = countsAsRecord, emotion = TodayMood.HAPPY, ) }