From 98ac361f0a690681dd7c430a44fe5daf37f46f2b Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Sun, 23 Aug 2026 20:34:58 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(mindrecord):=20=EC=A3=BC=EC=B0=A8=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=201=EB=85=84=EC=9C=BC=EB=A1=9C=20=EB=84=93?= =?UTF-8?q?=ED=9E=88=EA=B3=A0=20=EB=A9=94=EB=89=B4=EB=A5=BC=205=EA=B0=9C?= =?UTF-8?q?=20=EB=86=92=EC=9D=B4=EB=A1=9C=20=EB=AC=B6=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주차 드롭다운이 이번 주 포함 5개로 끝나, 시안(node 700-35071)이 표현한 추가 과거 리포트와 세로 스크롤을 쓸 수 없었다. 원인은 "한 화면에 보이는 항목 수" 를 그대로 선택지 개수(`WEEK_OPTION_COUNT = 5`)로 쓴 것이다 — 5개뿐이라 더 내려갈 항목이 없어 스크롤도 나타나지 않았다. 두 값을 분리했다. 선택 가능한 범위는 `WEEK_OPTION_COUNT = 52`(1년)로 넓히고, 가시 항목 수는 `DropdownMenu` 의 `heightIn(max = 항목 5개 + 여백)` 이 정한다. 메뉴 내용은 이미 세로 스크롤이라 높이만 묶으면 나머지가 스크롤로 넘어간다. `buildWeekOptions` 와 상수는 `WeeklyReportWeekOptions.kt` 로 옮겨, 같은 패키지의 `WeeklyReportRecordedDays.kt` 처럼 ViewModel 없이 직접 검증할 수 있게 했다. Closes #729 --- .../mindrecord/presentation/build.gradle.kts | 8 ++ .../component/WeeklyReportReviewCard.kt | 19 +++++ .../viewmodel/WeeklyReportViewModel.kt | 15 +--- .../viewmodel/WeeklyReportWeekOptions.kt | 32 ++++++++ .../component/WeeklyReportWeekMenuTest.kt | 82 +++++++++++++++++++ .../viewmodel/WeeklyReportWeekOptionsTest.kt | 52 ++++++++++++ 6 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportWeekOptions.kt create mode 100644 feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/component/WeeklyReportWeekMenuTest.kt create mode 100644 feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportWeekOptionsTest.kt diff --git a/feature/mindrecord/presentation/build.gradle.kts b/feature/mindrecord/presentation/build.gradle.kts index 76d0c5e121..c2b21718bf 100644 --- a/feature/mindrecord/presentation/build.gradle.kts +++ b/feature/mindrecord/presentation/build.gradle.kts @@ -7,6 +7,9 @@ plugins { android { namespace = "com.afternote.feature.mindrecord.presentation" resourcePrefix = "mindrecord_" + + // Robolectric 이 컴파일된 리소스로 화면을 띄운다 (#729). + testOptions.unitTests.isIncludeAndroidResources = true } dependencies { @@ -19,4 +22,9 @@ dependencies { implementation(libs.compose.rich.editor) testImplementation(libs.coroutines.test) + + // 열린 주차 메뉴의 스크롤·최하단 선택을 JVM 에서 그대로 재현한다 (#729). + testImplementation(libs.robolectric) + testImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/WeeklyReportReviewCard.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/WeeklyReportReviewCard.kt index 8b41609e64..fe1af8b9d1 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/WeeklyReportReviewCard.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/component/WeeklyReportReviewCard.kt @@ -10,6 +10,7 @@ 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.DropdownMenu @@ -121,13 +122,18 @@ fun WeeklyReportReviewCard( ) } + // 시안(node 700-35071)의 열린 메뉴는 항목 5개 높이에서 잘리고 오른쪽에 세로 + // 스크롤 표시가 있다 — 가시 항목 수보다 선택지가 많다는 표현이다. DropdownMenu + // 의 내용은 이미 세로 스크롤이라, 높이만 묶으면 나머지가 스크롤로 넘어간다 (#729). DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, containerColor = Color.White, + modifier = Modifier.heightIn(max = WeekMenuMaxHeight), ) { weekOptions.forEach { option -> DropdownMenuItem( + modifier = Modifier.height(WeekMenuItemHeight), text = { Text( text = weekLabel(option.monday), @@ -183,6 +189,19 @@ fun WeeklyReportReviewCard( } } +/** + * 주차 메뉴 한 항목의 높이. Material3 `DropdownMenuItem` 의 기본 최소 높이와 같은 값을 + * 고정으로 못 박아, 가시 항목 수([WeekMenuMaxHeight])가 항목 내용에 따라 흔들리지 않게 한다. + */ +private val WeekMenuItemHeight = 48.dp + +/** + * 열린 메뉴의 최대 높이 = 항목 5개 + `DropdownMenu` 내용의 위아래 여백(각 8dp). + * + * 선택지가 5개를 넘으면 여기서 잘리고 나머지는 세로 스크롤로 넘어간다 (#729). + */ +private val WeekMenuMaxHeight = WeekMenuItemHeight * 5 + 16.dp + @Composable private fun weekLabel(monday: LocalDate): String = stringResource( diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportViewModel.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportViewModel.kt index ae05e405cc..4199b31cca 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportViewModel.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportViewModel.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import java.time.DayOfWeek import java.time.LocalDate import java.time.format.DateTimeFormatter import javax.inject.Inject @@ -42,7 +41,7 @@ class WeeklyReportViewModel private val userRepository: UserRepository, ) : ViewModel() { private val weekOptions: List = - buildWeekOptions(today = LocalDate.now(), count = WEEK_OPTION_COUNT) + buildWeekOptions(today = LocalDate.now()) private val internalState = MutableStateFlow(InternalState()) private var loadJob: Job? = null @@ -129,16 +128,6 @@ class WeeklyReportViewModel } } - private fun buildWeekOptions( - today: LocalDate, - count: Int, - ): List { - val thisMonday = today.with(DayOfWeek.MONDAY) - return (0 until count).map { weeksAgo -> - WeekOption(monday = thisMonday.minusWeeks(weeksAgo.toLong())) - } - } - /** * 월~일 7칸을 만든다. * @@ -228,8 +217,6 @@ class WeeklyReportViewModel } companion object { - private const val WEEK_OPTION_COUNT = 5 - private val API_DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") private val RANGE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd.") diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportWeekOptions.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportWeekOptions.kt new file mode 100644 index 0000000000..58ddaa8893 --- /dev/null +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportWeekOptions.kt @@ -0,0 +1,32 @@ +package com.afternote.feature.mindrecord.presentation.viewmodel + +import java.time.DayOfWeek +import java.time.LocalDate + +/** + * 주차 드롭다운이 제공하는 과거 리포트 범위 (이번 주 포함 52주 = 1년). + * + * 종전 값 `5` 는 "한 화면에 보이는 항목 수" 를 그대로 선택지 개수로 쓴 것이라, 시안이 + * 표현한 추가 과거 리포트와 세로 스크롤을 쓸 수 없었다 (#729). **가시 항목 수는 메뉴 + * 높이가 정하고**(`WeeklyReportReviewCard`), 여기서는 선택 가능한 범위만 정한다. + * + * 서버에 가입일이 없어(`GET /users/me` 는 이름·이메일·전화·프로필만 준다) 계정 생성 주까지로 + * 좁힐 수 없다. 기록이 없는 주를 골라도 리포트는 0 건으로 정상 응답하므로 1년을 상한으로 뒀다. + */ +internal const val WEEK_OPTION_COUNT = 52 + +/** + * 이번 주 월요일부터 과거로 [count] 주를 최신순으로 만든다. + * + * 첫 원소가 언제나 이번 주라 진입 시 로드 대상([WeeklyReportViewModel.init])과 + * 실패 시 폴백 기준이 된다. + */ +internal fun buildWeekOptions( + today: LocalDate, + count: Int = WEEK_OPTION_COUNT, +): List { + val thisMonday = today.with(DayOfWeek.MONDAY) + return (0 until count).map { weeksAgo -> + WeekOption(monday = thisMonday.minusWeeks(weeksAgo.toLong())) + } +} diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/component/WeeklyReportWeekMenuTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/component/WeeklyReportWeekMenuTest.kt new file mode 100644 index 0000000000..56bbd59f22 --- /dev/null +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/component/WeeklyReportWeekMenuTest.kt @@ -0,0 +1,82 @@ +package com.afternote.feature.mindrecord.presentation.component + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import com.afternote.core.ui.theme.AfternoteTheme +import com.afternote.feature.mindrecord.presentation.viewmodel.WeekOption +import com.afternote.feature.mindrecord.presentation.viewmodel.buildWeekOptions +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.time.LocalDate + +/** + * 열린 주차 메뉴의 스크롤·최하단 선택 회귀 가드 (#729). + * + * 시안(node 700-35071)의 열린 메뉴는 항목 5개 높이에서 잘리고 오른쪽에 세로 스크롤 + * 표시가 있다. 선택지가 5개에서 끝나면 스크롤할 것이 없어 이 시연 자체가 불가능하다. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class WeeklyReportWeekMenuTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + private val today = LocalDate.of(2026, 8, 23) + private val options: List = buildWeekOptions(today = today) + + private fun label(monday: LocalDate): String = "${monday.monthValue}월 ${(monday.dayOfMonth - 1) / 7 + 1}주차 리포트" + + private fun openMenu(onWeekSelect: (LocalDate) -> Unit = {}) { + composeRule.setContent { + AfternoteTheme { + WeeklyReportReviewCard( + selectedMonday = options.first().monday, + weekOptions = options, + onWeekSelect = onWeekSelect, + ) + } + } + composeRule.onNodeWithText(label(options.first().monday)).performClick() + } + + @Test + fun `열린 메뉴에 5개를 넘는 선택지가 실린다`() { + openMenu() + + // 6번째 항목은 메뉴 높이 밖이라 스크롤해야 닿는다 — 존재 자체는 트리에 있다. + composeRule.onNodeWithText(label(options[5].monday)).performScrollTo().assertIsDisplayed() + } + + @Test + fun `스크롤해서 최하단 주차를 고르면 그 월요일이 전달된다`() { + var selected: LocalDate? = null + openMenu { selected = it } + + val bottom = options.last().monday + composeRule.onNodeWithText(label(bottom)).performScrollTo().performClick() + + assertEquals(bottom, selected) + assertEquals(LocalDate.of(2026, 8, 17).minusWeeks(51), selected) + } + + @Test + fun `이미 보고 있는 주를 다시 고르면 재조회하지 않는다`() { + var calls = 0 + openMenu { calls++ } + + // 같은 문구가 앵커와 메뉴 항목 두 곳에 있다 — 메뉴 쪽(마지막)을 누른다. + val nodes = composeRule.onAllNodesWithText(label(options.first().monday)) + nodes[nodes.fetchSemanticsNodes().lastIndex].performClick() + + assertEquals(0, calls) + } +} diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportWeekOptionsTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportWeekOptionsTest.kt new file mode 100644 index 0000000000..edb88c2bce --- /dev/null +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/WeeklyReportWeekOptionsTest.kt @@ -0,0 +1,52 @@ +package com.afternote.feature.mindrecord.presentation.viewmodel + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.time.DayOfWeek +import java.time.LocalDate + +/** + * 주차 선택지 범위 회귀 가드 (#729). + * + * 종전에는 "한 화면에 보이는 항목 수" 5 를 그대로 선택지 개수로 써서, 시안이 표현한 + * 추가 과거 리포트와 세로 스크롤을 쓸 수 없었다. 가시 항목 수와 선택 가능한 범위를 + * 분리했다는 사실을 여기서 고정한다. + */ +class WeeklyReportWeekOptionsTest { + @Test + fun `선택지는 한 화면 가시 항목 수보다 많다`() { + // 5 로 되돌리면 여기서 먼저 깨진다 — 스크롤할 항목이 없어지는 지점이다. + assertTrue("선택지가 가시 항목 수(5)를 넘어야 스크롤이 생긴다", WEEK_OPTION_COUNT > 5) + assertEquals(52, WEEK_OPTION_COUNT) + } + + @Test + fun `이번 주 월요일부터 과거로 최신순으로 만든다`() { + // 2026-08-23 은 일요일 — 그 주 월요일은 2026-08-17 이다. + val options = buildWeekOptions(today = LocalDate.of(2026, 8, 23)) + + assertEquals(WEEK_OPTION_COUNT, options.size) + assertEquals(LocalDate.of(2026, 8, 17), options.first().monday) + assertTrue(options.all { it.monday.dayOfWeek == DayOfWeek.MONDAY }) + assertEquals( + options.map { it.monday }.sortedDescending(), + options.map { it.monday }, + ) + } + + @Test + fun `최하단 선택지는 1년 전 주다`() { + val options = buildWeekOptions(today = LocalDate.of(2026, 8, 23)) + + assertEquals(LocalDate.of(2026, 8, 17).minusWeeks(51), options.last().monday) + } + + @Test + fun `월요일에 들어와도 그 주가 첫 선택지다`() { + // `with(DayOfWeek.MONDAY)` 는 그 주의 월요일이라 월요일 당일이면 자기 자신이다. + val options = buildWeekOptions(today = LocalDate.of(2026, 8, 17)) + + assertEquals(LocalDate.of(2026, 8, 17), options.first().monday) + } +} From 00dba6b32fa7b596fda8e4b337692678f9802db3 Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Mon, 24 Aug 2026 15:57:52 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(mindrecord):=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=82=AD=EC=A0=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20loader=20=EC=83=9D=EC=84=B1=EC=9E=90?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=98=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit develop 에서 넘어온 컴파일 깨짐(#1013 과 같은 건) 을 이 브랜치에서도 해소한다. Co-Authored-By: Claude Opus 5 (1M context) --- .../presentation/viewmodel/DraftListDeleteTest.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt index cfcf947974..cb93f3c8c5 100644 --- a/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt +++ b/feature/mindrecord/presentation/src/test/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DraftListDeleteTest.kt @@ -100,6 +100,12 @@ class DraftListDeleteTest { private fun viewModel(dailyQuestionRepository: DailyQuestionRepository): DraftListViewModel = DraftListViewModel( + // #769 가 목록 조회를 loader 로 뽑아낸 뒤 생성자가 3개로 늘었다. + loader = + MindRecordDraftLoader( + diaryRepository = EmptyDiaryDraftRepository, + dailyQuestionRepository = dailyQuestionRepository, + ), diaryRepository = EmptyDiaryDraftRepository, dailyQuestionRepository = dailyQuestionRepository, )