Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion feature/mindrecord/presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ plugins {

android {
namespace = "com.afternote.feature.mindrecord.presentation"
resourcePrefix = "mindrecord_"

// Robolectric 이 컴파일된 리소스로 화면을 띄운다 (#729).
testOptions.unitTests.isIncludeAndroidResources = true
resourcePrefix = "mindrecord_"
}

dependencies {
Expand All @@ -23,5 +24,8 @@ dependencies {
testImplementation(libs.coroutines.test)

// 컴파일된 리소스로 문구를 검증한다 — aapt2 의 앞뒤 공백 제거는 소스 XML 만 봐서는 잡히지 않는다 (#732).
// 열린 주차 메뉴의 스크롤·최하단 선택도 JVM 에서 그대로 재현한다 (#729).
testImplementation(libs.robolectric)
testImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시안 대조 재량 기록 (조치 불요, 노드 700-35071 직접 렌더·메타데이터 실측):

  • 시안 우측의 세로 스크롤바 인디케이터는 미구현 — Compose DropdownMenu 에 네이티브 스크롤바가 없고, 위 주석의 "가시 항목 수보다 선택지가 많다는 표현" 해석에 동의합니다.
  • 시안 메뉴는 카드 전폭(350pt)·항목 54pt, 구현은 내용 폭·48dp — 폭은 이 PR 이전부터의 편차(메뉴 폭 미지정)라 범위 밖이고, 항목 높이는 M3 기본과 정합한 재량으로 봅니다.

) {
weekOptions.forEach { option ->
DropdownMenuItem(
modifier = Modifier.height(WeekMenuItemHeight),
text = {
Text(
text = weekLabel(option.monday),
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,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
Expand All @@ -43,7 +42,7 @@ class WeeklyReportViewModel
private val userRepository: UserRepository,
) : ViewModel() {
private val weekOptions: List<WeekOption> =
buildWeekOptions(today = LocalDate.now(), count = WEEK_OPTION_COUNT)
buildWeekOptions(today = LocalDate.now())

private val internalState = MutableStateFlow(InternalState())
private var loadJob: Job? = null
Expand Down Expand Up @@ -131,16 +130,6 @@ class WeeklyReportViewModel
}
}

private fun buildWeekOptions(
today: LocalDate,
count: Int,
): List<WeekOption> {
val thisMonday = today.with(DayOfWeek.MONDAY)
return (0 until count).map { weeksAgo ->
WeekOption(monday = thisMonday.minusWeeks(weeksAgo.toLong()))
}
}

/**
* 월~일 7칸을 만든다.
*
Expand Down Expand Up @@ -234,8 +223,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.")

Expand Down
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal] 최하단 구간 라벨에 연도가 없어 작년 주차가 올해 것처럼 읽힙니다.

범위가 1년이 되면서 메뉴 하단은 2025년 8·9월 구간인데 라벨 포맷(%1$d월 %2$d주차 리포트)에 연도가 없어, 오늘 기준 최하단 "8월 4주차 리포트"(2025-08-25)가 열린 메뉴 안에서는 이번 달의 다른 주차처럼 읽힙니다(상단은 "8월 3주차"). 연도는 선택한 뒤 dateRange("2025.08.25. - 2025.08.31.")로만 드러납니다.

라벨 중복은 없습니다 — 52주 창에서 달 이름이 겹치는 건 상·하단의 같은 달뿐이고, 하단(작년) 주차 인덱스는 항상 상단(올해) 같은 달의 최대 인덱스보다 큽니다(52주=364일이라 최하단이 같은 달 8~9일 뒤 날짜로 떨어짐). 에뮬레이터 실측도 상단 8월 1·2·3주차 ↔ 하단 8월 4주차로 분리돼 나왔습니다.

시안 라벨 포맷 자체가 연도 없는 형태라 미확정 영역이고, FE 재량 규약대로 비차단입니다 — 작년 구간에만 연도를 병기할지는 후속 판단거리로 남깁니다.


/**
* 이번 주 월요일부터 과거로 [count] 주를 최신순으로 만든다.
*
* 첫 원소가 언제나 이번 주라 진입 시 로드 대상([WeeklyReportViewModel.init])과
* 실패 시 폴백 기준이 된다.
*/
internal fun buildWeekOptions(
today: LocalDate,
count: Int = WEEK_OPTION_COUNT,
): List<WeekOption> {
val thisMonday = today.with(DayOfWeek.MONDAY)
return (0 until count).map { weeksAgo ->
WeekOption(monday = thisMonday.minusWeeks(weeksAgo.toLong()))
}
}
Original file line number Diff line number Diff line change
@@ -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<ComponentActivity>()

private val today = LocalDate.of(2026, 8, 23)
private val options: List<WeekOption> = 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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading