From e77e33ff085ce4203f34b559754c3c0b8c491e42 Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Mon, 24 Aug 2026 14:40:36 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(mindrecord):=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD=EC=97=90=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B8=20=EC=88=98=EC=8B=A0=EC=9E=90=EB=A5=BC=20=EC=8B=A4?= =?UTF-8?q?=EC=96=B4=20=EB=B3=B4=EB=82=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이어쓰기·수정 등록은 `PATCH /diary/{id}` 를 타는데 그 본문에 receiverIds 가 없었다. 수신자 시트는 draft 모드에서도 그대로 열리므로, 사용자는 "채연님에게" 로 바뀐 화면을 보고 등록하고 성공 표시까지 받는데 서버 저장분은 수신자 0명이었다. 반대로 `date` 는 서버 스키마(DiaryUpdateRequest = title·content·isDraft·todayMood· receiverIds)에 없는 필드인데 보내고 있었다 — 실측으로 무시되는 것을 확인하고 뺀다. 빈 선택은 빈 목록이 아니라 null 로 보낸다. 서버는 빈 목록을 전체 해제로 읽으므로, 수신자를 건드리지 않은 편집이 기존 지정을 지워 버린다 — 목록 응답에 수신자가 없어 프리필로 되살릴 수도 없다. 생성 경로와 같은 규칙이다. Co-Authored-By: Claude Opus 5 (1M context) --- .../feature/mindrecord/data/dto/DiaryDto.kt | 3 +- .../mindrecord/data/mapper/DiaryMapper.kt | 2 +- .../data/mapper/DiaryUpdateRequestTest.kt | 75 +++++++++++++++++++ .../mindrecord/domain/model/DiaryModels.kt | 9 ++- .../viewmodel/DiaryWriteViewModel.kt | 5 +- 5 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryUpdateRequestTest.kt diff --git a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DiaryDto.kt b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DiaryDto.kt index 850e816c55..e492cc1ec9 100644 --- a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DiaryDto.kt +++ b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DiaryDto.kt @@ -31,8 +31,9 @@ data class DiaryUpdateRequestDto( @SerialName("content") val content: String, @SerialName("isDraft") val isDraft: Boolean, @SerialName("todayMood") val todayMood: TodayMoodDto, - @SerialName("date") val date: String, @SerialName("imageUrl") val imageUrl: String? = null, + /** null 이면 기존 수신자 유지, 빈 목록이면 전체 해제 (서버 규칙). */ + @SerialName("receiverIds") val receiverIds: List? = null, ) @Serializable diff --git a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryMapper.kt b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryMapper.kt index 4dbc6d7873..4ff9e4ce5a 100644 --- a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryMapper.kt +++ b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryMapper.kt @@ -58,6 +58,6 @@ fun DiaryUpdatePayload.toRequest(): DiaryUpdateRequestDto = content = content, isDraft = isDraft, todayMood = todayMood.toDto(), - date = date, imageUrl = imageUrl, + receiverIds = receiverIds, ) diff --git a/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryUpdateRequestTest.kt b/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryUpdateRequestTest.kt new file mode 100644 index 0000000000..f7df740bb7 --- /dev/null +++ b/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryUpdateRequestTest.kt @@ -0,0 +1,75 @@ +package com.afternote.feature.mindrecord.data.mapper + +import com.afternote.feature.mindrecord.domain.model.DiaryUpdatePayload +import com.afternote.feature.mindrecord.domain.model.TodayMood +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * 일기 수정 요청의 와이어 계약 가드 (#955). + * + * 이어쓰기 등록은 `PATCH /diary/{id}` 를 타는데, 이 본문에 `receiverIds` 가 없어서 + * **수신자를 골라 등록해도 0명으로 발행**되고 화면은 성공으로 표시됐다. 반대로 `date` 는 + * 서버 스키마에 없는 필드인데 보내고 있었다(실측: 보내도 무시된다). + * + * 매퍼 반환값이 아니라 **직렬화된 본문**을 본다 — 실제로 서버가 받는 것이 그것이다. + */ +class DiaryUpdateRequestTest { + /** 앱이 실제로 쓰는 설정과 같게 둔다 (core:network NetworkModule). */ + private val json = + Json { + ignoreUnknownKeys = true + coerceInputValues = true + } + + private fun bodyOf(receiverIds: List?) = + json.encodeToString( + DiaryUpdatePayload( + title = "제목", + content = "

본문

", + isDraft = false, + todayMood = TodayMood.HAPPY, + receiverIds = receiverIds, + ).toRequest(), + ) + + @Test + fun `고른 수신자가 요청 본문에 실린다`() { + assertTrue(bodyOf(listOf(17L, 18L)).contains("\"receiverIds\":[17,18]")) + } + + @Test + fun `수신자를 안 골랐으면 receiverIds 를 아예 보내지 않는다`() { + // 서버는 빈 목록을 "전체 해제" 로 읽는다. 안 고른 것을 해제로 보내면 수신자를 + // 건드리지 않은 편집이 기존 지정을 지운다. + assertFalse(bodyOf(null).contains("receiverIds")) + } + + @Test + fun `서버 스키마에 없는 date 는 보내지 않는다`() { + assertFalse(bodyOf(null).contains("\"date\"")) + } + + @Test + fun `나머지 필드는 그대로 실린다`() { + val request = + DiaryUpdatePayload( + title = "제목", + content = "

본문

", + isDraft = true, + todayMood = TodayMood.SAD, + imageUrl = "https://cdn.example.com/a.png", + receiverIds = listOf(17L), + ).toRequest() + + assertEquals("제목", request.title) + assertEquals("

본문

", request.content) + assertTrue(request.isDraft) + assertEquals("https://cdn.example.com/a.png", request.imageUrl) + assertEquals(listOf(17L), request.receiverIds) + } +} diff --git a/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/DiaryModels.kt b/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/DiaryModels.kt index 0533022f30..c18078b0d0 100644 --- a/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/DiaryModels.kt +++ b/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/DiaryModels.kt @@ -36,6 +36,13 @@ data class DiaryUpdatePayload( val content: String, val isDraft: Boolean, val todayMood: TodayMood, - val date: String, val imageUrl: String? = null, + /** + * 이 일기를 전달받을 수신자 ID 목록. 생성 경로와 같은 형태다 (#955). + * + * 서버 규칙: **null 이면 기존 수신자를 그대로 두고, 빈 목록이면 전체 해제**한다 (실측). + * 그래서 "고른 게 없음" 을 빈 목록으로 보내면 안 된다 — 수신자를 건드리지 않은 편집이 + * 기존 지정을 지워 버린다. + */ + val receiverIds: List? = null, ) diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt index 19606eaa76..eab7e88350 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt @@ -108,8 +108,11 @@ class DiaryWriteViewModel content = state.content, isDraft = isDraft, todayMood = mood, - date = state.date.toString(), imageUrl = state.imageUrl, + // 생성 경로와 같은 규칙. 빈 선택을 빈 목록으로 보내면 서버가 + // 전체 해제로 읽어, 수신자를 건드리지 않은 편집이 기존 지정을 + // 지운다 — 목록 응답에 수신자가 없어 되살릴 수도 없다. + receiverIds = state.selectedReceiverIds.toList().takeIf { it.isNotEmpty() }, ), ) } else { From 1eab335a3c670c6c17331bf4851e04be3703fc66 Mon Sep 17 00:00:00 2001 From: Sadturtleman Date: Mon, 24 Aug 2026 16:43:45 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(mindrecord):=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=97=90=EC=84=9C=20=EA=B3=84=EC=95=BD=20?= =?UTF-8?q?=EB=B0=96=20imageUrl=20=EB=8F=84=20=EB=BA=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit date 를 뺀 근거가 그대로 적용된다 — BE DiaryUpdateRequest 는 title·content·isDraft· todayMood·receiverIds 뿐이고 Diary.imageUrl 에 대입하는 코드도 없다. encodeDefaults 가 꺼져 있어 평소엔 키가 안 나가지만, 이미지를 붙인 편집에서는 실제로 실려 나갔다. «나머지 필드는 그대로 실린다» 도 매퍼 반환값 대신 직렬화 본문을 보게 바꿔, 클래스가 선언한 «서버가 실제로 받는 것을 본다» 와 방식을 맞춘다. 리뷰 지적. Co-Authored-By: Claude Opus 5 (1M context) --- .../feature/mindrecord/data/dto/DiaryDto.kt | 1 - .../mindrecord/data/mapper/DiaryMapper.kt | 1 - .../data/mapper/DiaryUpdateRequestTest.kt | 38 +++++++++++-------- .../mindrecord/domain/model/DiaryModels.kt | 1 - .../viewmodel/DiaryWriteViewModel.kt | 1 - 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DiaryDto.kt b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DiaryDto.kt index e492cc1ec9..9ec42fc141 100644 --- a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DiaryDto.kt +++ b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/dto/DiaryDto.kt @@ -31,7 +31,6 @@ data class DiaryUpdateRequestDto( @SerialName("content") val content: String, @SerialName("isDraft") val isDraft: Boolean, @SerialName("todayMood") val todayMood: TodayMoodDto, - @SerialName("imageUrl") val imageUrl: String? = null, /** null 이면 기존 수신자 유지, 빈 목록이면 전체 해제 (서버 규칙). */ @SerialName("receiverIds") val receiverIds: List? = null, ) diff --git a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryMapper.kt b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryMapper.kt index 4ff9e4ce5a..c90ac91edd 100644 --- a/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryMapper.kt +++ b/feature/mindrecord/data/src/main/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryMapper.kt @@ -58,6 +58,5 @@ fun DiaryUpdatePayload.toRequest(): DiaryUpdateRequestDto = content = content, isDraft = isDraft, todayMood = todayMood.toDto(), - imageUrl = imageUrl, receiverIds = receiverIds, ) diff --git a/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryUpdateRequestTest.kt b/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryUpdateRequestTest.kt index f7df740bb7..7b0091c902 100644 --- a/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryUpdateRequestTest.kt +++ b/feature/mindrecord/data/src/test/kotlin/com/afternote/feature/mindrecord/data/mapper/DiaryUpdateRequestTest.kt @@ -4,7 +4,6 @@ import com.afternote.feature.mindrecord.domain.model.DiaryUpdatePayload import com.afternote.feature.mindrecord.domain.model.TodayMood import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -56,20 +55,29 @@ class DiaryUpdateRequestTest { @Test fun `나머지 필드는 그대로 실린다`() { - val request = - DiaryUpdatePayload( - title = "제목", - content = "

본문

", - isDraft = true, - todayMood = TodayMood.SAD, - imageUrl = "https://cdn.example.com/a.png", - receiverIds = listOf(17L), - ).toRequest() + val body = + json.encodeToString( + DiaryUpdatePayload( + title = "제목", + content = "

본문

", + isDraft = true, + todayMood = TodayMood.SAD, + receiverIds = listOf(17L), + ).toRequest(), + ) - assertEquals("제목", request.title) - assertEquals("

본문

", request.content) - assertTrue(request.isDraft) - assertEquals("https://cdn.example.com/a.png", request.imageUrl) - assertEquals(listOf(17L), request.receiverIds) + assertTrue(body.contains("\"title\":\"제목\"")) + assertTrue(body.contains("\"content\":\"

본문

\"")) + assertTrue(body.contains("\"isDraft\":true")) + assertTrue(body.contains("\"todayMood\":\"SAD\"")) + assertTrue(body.contains("\"receiverIds\":[17]")) + } + + @Test + fun `서버 스키마에 없는 imageUrl 도 보내지 않는다`() { + // date 를 뺀 근거가 그대로 적용된다 — BE `DiaryUpdateRequest` 는 + // [title, content, isDraft, todayMood, receiverIds] 뿐이고 `Diary.imageUrl` 에 + // 대입하는 코드도 없다. 화면이 이미지를 붙인 편집에서만 실려 나가던 자리다. + assertFalse(bodyOf(null).contains("\"imageUrl\"")) } } diff --git a/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/DiaryModels.kt b/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/DiaryModels.kt index c18078b0d0..a8f57dbedf 100644 --- a/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/DiaryModels.kt +++ b/feature/mindrecord/domain/src/main/kotlin/com/afternote/feature/mindrecord/domain/model/DiaryModels.kt @@ -36,7 +36,6 @@ data class DiaryUpdatePayload( val content: String, val isDraft: Boolean, val todayMood: TodayMood, - val imageUrl: String? = null, /** * 이 일기를 전달받을 수신자 ID 목록. 생성 경로와 같은 형태다 (#955). * diff --git a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt index eab7e88350..f6979fc77f 100644 --- a/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt +++ b/feature/mindrecord/presentation/src/main/kotlin/com/afternote/feature/mindrecord/presentation/viewmodel/DiaryWriteViewModel.kt @@ -108,7 +108,6 @@ class DiaryWriteViewModel content = state.content, isDraft = isDraft, todayMood = mood, - imageUrl = state.imageUrl, // 생성 경로와 같은 규칙. 빈 선택을 빈 목록으로 보내면 서버가 // 전체 해제로 읽어, 수신자를 건드리지 않은 편집이 기존 지정을 // 지운다 — 목록 응답에 수신자가 없어 되살릴 수도 없다.