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
11 changes: 11 additions & 0 deletions docs/qa/assumptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,17 @@ Compose 테스트로 고정했다.
스키마에 `date` 없음, 보내도 무시됨을 실측). 날짜 선택이 표시 전용인 문제는 BE 계약
확장이나 기획 결정이 필요해 #1008 로 분리했다.

## #1008 — 일기 기록 날짜를 표시 전용으로 (판단 근거)

서버가 `date` 를 받지 않는다는 것은 추측이 아니라 실측이다 (2026-08-25).

- `POST /api/v1/diary` 에 `date: "2026-08-01"` → 저장분 `date: "2026-08-25"` (diaryId 41545)
- `PATCH /api/v1/diary/41545` 에 같은 값 → 저장분 그대로 `2026-08-25`
- Swagger 스키마의 요청 필드: `title · content · isDraft · todayMood · receiverIds` (양쪽 모두 `date` 없음)

**추측인 부분은 «어느 쪽으로 맞출지» 다.** 이슈가 든 두 선택지 중 (b) 표시 전용을 택했다 —
(a) BE 확장은 우리 손 밖이고, 그때까지 «고를 수 있지만 반영되지 않는» 상태를 두면 사용자가
고른 날짜로 저장됐다고 믿는다. BE 가 `date` 를 수용하면 피커를 되돌린다.
## #613 — 수신자 홈 설정 진입 (판단 보류)

- 시안(수신자 섹션 `4327:73626`)의 수신자 홈 헤더에는 **톱니만** 있고 프로필 아이콘이 없다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
Expand Down Expand Up @@ -46,7 +45,6 @@ import com.afternote.core.ui.theme.AfternoteDesign
import com.afternote.core.ui.theme.AfternoteTheme
import com.afternote.core.ui.topbar.DetailTopBar
import com.afternote.feature.mindrecord.domain.model.TodayMood
import com.afternote.feature.mindrecord.presentation.component.BottomSheetCalendar
import com.afternote.feature.mindrecord.presentation.component.ReceiverSelectBottomSheet
import com.afternote.feature.mindrecord.presentation.component.WriteTextField
import com.afternote.feature.mindrecord.presentation.viewmodel.DiaryWriteViewModel
Expand All @@ -65,7 +63,6 @@ fun DiaryWriteScreen(
viewModel: DiaryWriteViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var showPicker by remember { mutableStateOf(false) }
var showReceiverSheet by remember { mutableStateOf(false) }
val currentOnSubmitSuccess by rememberUpdatedState(onSubmitSuccess)

Expand Down Expand Up @@ -169,11 +166,15 @@ fun DiaryWriteScreen(
}

// Figma 2671:17922 — 날짜 선택 행 ("yyyy년 M월 d일" + 아래 화살표)
// **표시 전용이다.** 서버는 생성·수정 어느 쪽에서도 date 를 받지 않고 기록 날짜를
// 요청 시각으로 정한다 (실측 2026-08-25: POST·PATCH 에 date 를 실어도 저장분은
// 오늘, Swagger 스키마에도 필드가 없다). 고를 수 있게 두면 사용자는 고른 날짜로
// 남았다고 믿는데 실제로는 오늘로 저장된다 — 고르지 못하게 해 거짓을 없앤다 (#1008).
//
// BE 가 date 를 수용하면 이 자리에 피커를 되돌린다. 이어쓰기 프리필이 채워 주는
// 원래 날짜를 보여 주는 역할은 그대로다.
Row(
modifier =
Modifier
.fillMaxWidth()
.clickable { showPicker = !showPicker },
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Expand All @@ -182,12 +183,6 @@ fun DiaryWriteScreen(
style = AfternoteDesign.typography.captionLargeR,
color = AfternoteDesign.colors.gray9,
)
IconButton(onClick = { showPicker = !showPicker }) {
Icon(
painter = painterResource(R.drawable.core_ui_arrowdown),
contentDescription = null,
)
}
}

HorizontalDivider()
Expand Down Expand Up @@ -307,17 +302,6 @@ fun DiaryWriteScreen(
)
}

if (showPicker) {
BottomSheetCalendar(
initialDate = uiState.date,
onDismiss = { showPicker = false },
onDateSelect = { date ->
viewModel.onDateSelected(date)
showPicker = false
},
)
}

if (showReceiverSheet) {
ReceiverSelectBottomSheet(
receivers = uiState.receivers,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ data class DiaryWriteUiState(
val title: String = "",
val content: String = "",
val mood: TodayMood? = null,
/**
* 화면에 표시할 기록 날짜. **표시 전용이다** — 서버는 생성·수정 어느 쪽에서도 이 값을
* 받지 않고 기록 날짜를 요청 시각으로 정한다 (#1008). 이어쓰기 프리필이 원래 날짜를
* 채워 주는 자리라 상태 자체는 남긴다.
*/
val date: LocalDate = LocalDate.now(),
val imageUrl: String? = null,
/** `GET /users/receivers` 로 불러온 내 수신인 목록. 로드 실패 시 빈 목록 (작성은 가능). */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ class DiaryWriteViewModel
_uiState.update { it.copy(mood = mood) }
}

fun onDateSelected(date: LocalDate) {
_uiState.update { it.copy(date = date) }
}

fun onReceiverToggled(receiverId: Long) {
_uiState.update {
val selected = it.selectedReceiverIds
Expand Down Expand Up @@ -228,9 +224,8 @@ class DiaryWriteViewModel
title = draft.title,
content = draft.content,
mood = draft.todayMood,
// 서버가 날짜를 주지 않은 임시저장이면 날짜 선택기의 현재 값을
// 유지한다 — 표시용으로 오늘을 지어내는 것과 달리, 여기서는
// 사용자가 등록 전에 직접 고르는 값이다.
// 표시 전용 값이다 — 서버가 날짜를 주면 그걸 보여 주고, 안 주면
// 화면에 이미 떠 있던 값을 유지한다. 고르는 수단은 없다 (#1008).
date = draft.toUi()?.date ?: it.date,
imageUrl = draft.imageUrl,
isDraftLoading = false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.afternote.feature.mindrecord.presentation.screen

import com.afternote.feature.mindrecord.presentation.viewmodel.DiaryWriteViewModel
import org.junit.Assert.assertTrue
import org.junit.Test
import java.time.LocalDate

/**
* 일기 기록 날짜가 «고를 수 있지만 반영되지 않는» 상태가 아닌지 (#1008).
*
* 서버는 생성·수정 어느 쪽에서도 `date` 를 받지 않고 기록 날짜를 요청 시각으로 정한다
* (실측 2026-08-25: `POST`·`PATCH` 에 `date: "2026-08-01"` 을 실어 보내도 저장분은
* `2026-08-25`, Swagger 스키마에도 필드가 없다). 그 상태에서 날짜 피커를 열어 두면
* 사용자는 고른 날짜로 남았다고 믿는다.
*
* 화면에서 피커를 걷어냈으므로 **ViewModel 에 날짜를 바꾸는 창구가 남아 있으면 안 된다** —
* 남겨 두면 다음 사람이 그 함수를 보고 UI 를 되살린다. BE 가 `date` 를 수용하면 그때 함께
* 되돌린다.
*/
class DiaryWriteDateDisplayTest {
@Test
fun `날짜를 바꾸는 공개 창구가 없다`() {
// "update" 가 "date" 를 부분 문자열로 품는다 — 날짜와 무관한 updateXxx 가 하나만
// 생겨도 빨개지므로, 이름 규칙이 아니라 **LocalDate 를 받는 공개 함수**로 좁힌다.
val dateSetters =
DiaryWriteViewModel::class.java.methods
.filter {
java.lang.reflect.Modifier
.isPublic(it.modifiers)

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. 이 필터는 항상 참이다. Class.getMethods() 는 정의상 public 멤버만 돌려준다(상속분 포함). 그래서 isPublic 은 걸러 내는 것이 없고, ktlint 가 이 호출 때문에 줄을 셋으로 쪼갰다. 지우면 한 줄로 돌아온다.

2. 내가 제안한 타입 기반에는 구멍이 있다 — 짚어 두는 게 맞겠다. LocalDate 를 받는 함수만 보므로, 날짜 창구가 onDateSelected(millis: Long) 이나 onDateSelected(iso: String) 으로 되살아나면 이 가드는 침묵한다. 가상의 이야기만은 아니다 — 피커를 되살리는 사람은 BottomSheetCalendar 의 콜백 시그니처를 그대로 받아 적을 가능성이 높고, Compose 의 날짜 피커 계열은 epoch millis 를 넘기는 쪽이 흔하다.

이름과 타입을 둘 다 보면 오탐 없이 촘촘해진다. 이름 쪽은 앞서 문제가 된 update… 접두만 빼면 되고, 그 예외를 주석이 아니라 코드로 적어 두면 다음 사람이 왜 빠졌는지 알 수 있다.

val dateSetters =
    DiaryWriteViewModel::class.java.methods
        .filter { method ->
            method.parameterTypes.any { it == LocalDate::class.java } ||
                // "update" 가 "date" 를 부분 문자열로 품는다 — 그 접두만 빼고 이름으로도 본다.
                (method.name.contains("date", ignoreCase = true) && !method.name.startsWith("update"))
        }.map { it.name }

updateDate 같은 이름이 생기면 이 예외에 가려지지만, 그때는 LocalDate 인자를 받든 다른 타입을 받든 첫 조건이나 리뷰가 잡는다. 지금처럼 한 조건만 두는 것보다는 넓다.

}.filter { it.parameterTypes.any { type -> type == LocalDate::class.java } }
.map { it.name }

assertTrue("날짜 변경 창구가 남아 있다: $dateSetters", dateSetters.isEmpty())
}
}
Loading