From 7beabcbb61d5f3328956f351fcfa765945adb1df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Wed, 2 Sep 2026 20:50:13 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(afternote):=20=EA=B3=A1=EC=9D=84=20?= =?UTF-8?q?=EC=95=88=20=EB=8B=B4=EC=9D=80=20=EC=9E=84=EC=8B=9C=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=B6=94=EC=96=B5=20=EB=85=B8=ED=8A=B8=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=EA=B0=80=20=EC=97=B4=EB=A6=AC=EA=B2=8C=20playlist=20?= =?UTF-8?q?=EB=B6=80=EC=9E=AC=EB=A5=BC=20=EA=B4=80=EC=9A=A9=ED=95=9C?= =?UTF-8?q?=EB=8B=A4=20(#1745)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버 임시저장은 카테고리별 필수값 검증을 건너뛰어 곡을 한 곡도 안 담은 PLAYLIST 는 상세 응답에 playlist 자체가 없다. 기존 매퍼의 requireNotNull 이 던져 조회부터 실패하므로, 계정 정보와 같은 처방으로 빈 추모 노트로 낮춘다. isDraft 는 도메인으로 올리지 않는다 — FE 안에 그 값을 읽는 소비처가 없고, 타입을 갈라도 필드가 같아 얻는 게 없었다. 소비처(목록 진입 분기·저장 버튼 구성)가 생길 때 그 이슈에서 올린다. --- .../data/mapper/AfternoteDetailMapper.kt | 16 +++++++++- .../data/mapper/AfternoteDetailMapperTest.kt | 30 ++++++++++--------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/feature/afternote/data/src/main/kotlin/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapper.kt b/feature/afternote/data/src/main/kotlin/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapper.kt index eb59d71d93..eb4ae499ec 100644 --- a/feature/afternote/data/src/main/kotlin/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapper.kt +++ b/feature/afternote/data/src/main/kotlin/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapper.kt @@ -59,7 +59,7 @@ private fun AfternoteDetailDto.toDetailContent(type: AfternoteType): DetailConte } AfternoteType.MEMORIAL -> { - requireNotNull(memorial) { "playlist is required for MEMORIAL detail" }.toMemorialContent() + memorial?.toMemorialContent() ?: EMPTY_MEMORIAL_CONTENT } AfternoteType.ESTATE -> { @@ -67,6 +67,20 @@ private fun AfternoteDetailDto.toDetailContent(type: AfternoteType): DetailConte } } +/** + * 곡도 미디어도 아직 안 담은 추억 노트. + * + * 서버 임시저장(`isDraft=true`)은 카테고리별 필수값 검증을 건너뛰어(`AfternoteValidator`), 곡을 한 곡도 안 담은 + * PLAYLIST 는 `playlist` 자체가 안 온다. 여기서 던지면 그 임시저장은 이어쓰기 이전에 조회부터 실패해 영영 안 열린다. + * 발행 완료는 서버가 최소 1곡을 강제하므로(`requirePublishedPlaylist`) 이 폴백이 발행 상세에서 발동할 일은 없다 — + * 계정 정보와 같은 이유로, 빠진 값은 던지지 않고 빈 값으로 낮춘다. + */ +private val EMPTY_MEMORIAL_CONTENT = + DetailContent.Memorial( + songs = emptyList(), + media = MemorialMedia(photoUrl = null, videoUrl = null, thumbnailUrl = null), + ) + // 던지면 그 상세가 영영 안 열리므로 빠진 값은 빈 문자열로 낮춘다. private fun AfternoteDetailDto.toPublishedCredentials() = DetailCredentials( diff --git a/feature/afternote/data/src/test/java/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapperTest.kt b/feature/afternote/data/src/test/java/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapperTest.kt index 3ae50d2e54..5ed6bc9e4c 100644 --- a/feature/afternote/data/src/test/java/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapperTest.kt +++ b/feature/afternote/data/src/test/java/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapperTest.kt @@ -20,7 +20,7 @@ import org.junit.Test * [AfternoteDetailDto.toDomain] 회귀 가드 (작성자 상세). * 핵심 경계: 공통 필드와 타입별 [DetailContent] 분리, 부분·부재 credentials 의 빈 값 강등, * receivers null→emptyList, receiver 필드 null→"", processingMethods null→emptyList, - * memorialVideo null→video/thumbnail null. + * memorialVideo null→video/thumbnail null, playlist 부재(곡 없는 임시저장)→곡 0개·미디어 없음. */ class AfternoteDetailMapperTest { @Test @@ -271,20 +271,22 @@ class AfternoteDetailMapperTest { } @Test - fun `toDomain - playlist 타입에 playlist가 없으면 오류`() { - val exception = - assertThrows(IllegalArgumentException::class.java) { - AfternoteDetailDto( - isDraft = false, - receivers = emptyList(), - afternoteId = 1L, - category = "PLAYLIST", - title = "t", - updatedAt = UPDATED_AT, - ).toDomain() - } + fun `toDomain - 곡을 안 담은 임시저장 playlist 는 playlist 부재를 곡 0개·미디어 없음으로 낮춘다`() { + val result = + AfternoteDetailDto( + isDraft = true, + receivers = emptyList(), + afternoteId = 1L, + category = "PLAYLIST", + title = "t", + updatedAt = UPDATED_AT, + ).toDomain() - assertEquals("playlist is required for MEMORIAL detail", exception.message) + val memorial = result.content as DetailContent.Memorial + assertTrue(memorial.songs.isEmpty()) + assertNull(memorial.media.photoUrl) + assertNull(memorial.media.videoUrl) + assertNull(memorial.media.thumbnailUrl) } @Test From 8aacd539357033deb52c680bc38c13e0242c9345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Thu, 3 Sep 2026 03:56:05 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs(afternote):=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=83=81=EC=84=B8=20=ED=8F=B4=EB=B0=B1=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=EC=9D=84=20=EC=84=9C=EB=B2=84=20=EA=B3=84?= =?UTF-8?q?=EC=95=BD=20=EC=8B=A4=EC=B8=A1=EB=8C=80=EB=A1=9C=20=EA=B3=A0?= =?UTF-8?q?=EC=B9=9C=EB=8B=A4=20(#1745)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "영영 안 열린다" 가 현재 재현되는 결함처럼 읽혔다. 앱은 아직 생성 요청에 isDraft 를 싣지도, 목록에 draftOnly=true 를 보내지도 않아 임시저장 상세로 들어오는 경로가 없다 (상세 진입은 발행분만 오는 홈 목록 탭 하나뿐). 폴백이 필요한 근거를 실코드로 다시 적는다 — 서버 상세는 임시저장을 걸러 내지 않고 (AfternoteService.getDetailAfternote 는 소유자만 본다) 응답 형태로만 가르며 (AfternotedetailResponse.of 의 Draft 분기), 발행분 강제는 요청 검증기가 아니라 응답 조립의 requirePublishedPlaylist 가 한다. 발동 시점은 #808 이 이어쓰기를 붙일 때다. --- .../afternote/data/mapper/AfternoteDetailMapper.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/feature/afternote/data/src/main/kotlin/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapper.kt b/feature/afternote/data/src/main/kotlin/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapper.kt index eb4ae499ec..ab5da675b8 100644 --- a/feature/afternote/data/src/main/kotlin/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapper.kt +++ b/feature/afternote/data/src/main/kotlin/com/afternote/feature/afternote/data/mapper/AfternoteDetailMapper.kt @@ -70,9 +70,12 @@ private fun AfternoteDetailDto.toDetailContent(type: AfternoteType): DetailConte /** * 곡도 미디어도 아직 안 담은 추억 노트. * - * 서버 임시저장(`isDraft=true`)은 카테고리별 필수값 검증을 건너뛰어(`AfternoteValidator`), 곡을 한 곡도 안 담은 - * PLAYLIST 는 `playlist` 자체가 안 온다. 여기서 던지면 그 임시저장은 이어쓰기 이전에 조회부터 실패해 영영 안 열린다. - * 발행 완료는 서버가 최소 1곡을 강제하므로(`requirePublishedPlaylist`) 이 폴백이 발행 상세에서 발동할 일은 없다 — + * 서버 상세는 임시저장을 걸러 내지 않고(`AfternoteService.getDetailAfternote` 는 소유자만 본다) 응답 형태로만 + * 가른다(`AfternotedetailResponse.of` 의 `Draft` 분기). 임시저장(`isDraft=true`)은 카테고리별 필수값 검증을 + * 건너뛰므로(`AfternoteValidator`) 곡을 한 곡도 안 담은 PLAYLIST 는 `playlist` 자체가 안 온다 — #808 이 + * `draftOnly=true` 목록과 이어쓰기를 붙이면 그런 임시저장이 이 분기로 들어오고, 여기서 던지면 이어쓰기 이전에 + * 조회부터 실패한다. 발행 상세는 서버가 응답을 조립하면서 최소 1곡을 강제하므로 + * (`AfternotedetailResponse.requirePublishedPlaylist`) 이 폴백이 발행분을 빈 추모 노트로 위장할 일은 없다 — * 계정 정보와 같은 이유로, 빠진 값은 던지지 않고 빈 값으로 낮춘다. */ private val EMPTY_MEMORIAL_CONTENT =