From 3498e610a7b80490dd7c0e6a12fd315451c8b29e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Mon, 28 Sep 2026 15:03:08 +0900 Subject: [PATCH] =?UTF-8?q?refactor(afternote):=20=EB=AF=B8=EB=94=94?= =?UTF-8?q?=EC=96=B4=20URL=20=EB=B9=88=20=EA=B0=92=20=EC=A0=95=EA=B7=9C?= =?UTF-8?q?=ED=99=94=EB=A5=BC=20=EC=9E=85=EA=B5=AC=EB=A1=9C=20=EB=AA=A8?= =?UTF-8?q?=EC=9D=80=EB=8B=A4=20(#2207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상세 data 매퍼와 썸네일 결과 반영에서 빈 문자열을 null 로 맞추고, 폼 매퍼의 미디어 URL ifBlank 를 걷어낸다. --- .../data/mapper/AfternoteDetailMapper.kt | 25 +++++++++--------- .../data/mapper/AfternoteDetailMapperTest.kt | 26 ++++++++++++++++++- .../editor/AfternoteEditorFormMapper.kt | 16 ++++++------ .../editor/state/EditableMemorialVideo.kt | 4 +-- .../editor/state/EditableMemorialVideoTest.kt | 11 ++++++++ 5 files changed, 59 insertions(+), 23 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 96e9ddf4e0..d268c664d7 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 @@ -106,12 +106,7 @@ private fun AfternoteDetailDto.toDraftContent(type: AfternoteType): DraftContent AfternoteType.MEMORIAL -> { DraftContent.Memorial( songs = playlist?.songs?.map { it.toDomain() }.orEmpty(), - media = - MemorialMedia( - photoUrl = playlist?.memorialPhotoUrl, - videoUrl = playlist?.memorialVideo?.videoUrl, - thumbnailUrl = playlist?.memorialVideo?.thumbnailUrl, - ), + media = playlist.toMemorialMedia(), ) } @@ -183,12 +178,18 @@ private fun AfternoteDetailDto.toTimestamps(): DetailTimestamps = private fun AfternotePlaylistDto.toMemorialContent() = DetailContent.Memorial( songs = songs.map { it.toDomain() }, - media = - MemorialMedia( - photoUrl = memorialPhotoUrl, - videoUrl = memorialVideo?.videoUrl, - thumbnailUrl = memorialVideo?.thumbnailUrl, - ), + media = toMemorialMedia(), + ) + +/** + * 미디어 URL 의 빈 값은 여기서 한 번만 `null` 로 맞춘다. 앱 안에서는 `null` 만 「없음」이다. + * BE `main` 은 빈 미디어를 `null` 로 저장하므로 빈 문자열은 옛 데이터에서만 온다. + */ +private fun AfternotePlaylistDto?.toMemorialMedia() = + MemorialMedia( + photoUrl = this?.memorialPhotoUrl?.ifBlank { null }, + videoUrl = this?.memorialVideo?.videoUrl?.ifBlank { null }, + thumbnailUrl = this?.memorialVideo?.thumbnailUrl?.ifBlank { null }, ) private fun AfternoteDetailReceiverDto.toDomain(): DetailReceiver? = 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 f6f72c42ad..70dc4f1f72 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 @@ -19,7 +19,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, 미디어 URL 빈 문자열→null. */ class AfternoteDetailMapperTest { @Test @@ -231,6 +231,30 @@ class AfternoteDetailMapperTest { assertEquals("a", memorial.songs.single().artist) } + @Test + fun `toDomain - 미디어 URL 의 빈 문자열은 없음으로 맞춘다`() { + val result = + Json + .decodeFromString( + """ + { + "afternoteId":1,"category":"PLAYLIST","title":"t","isDraft":false, + "updatedAt":"$UPDATED_AT","receivers":[], + "playlist":{ + "memorialPhotoUrl":"", + "songs":[], + "memorialVideo":{"videoUrl":" ","thumbnailUrl":""} + } + } + """.trimIndent(), + ).toDomain() + + val media = (result.content as DetailContent.Memorial).media + assertNull(media.photoUrl) + assertNull(media.videoUrl) + assertNull(media.thumbnailUrl) + } + @Test fun `toDomain - playlist의 공통 leaveMessage도 보존한다`() { val result = diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/editor/AfternoteEditorFormMapper.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/editor/AfternoteEditorFormMapper.kt index 99997928a7..1bb1f767b3 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/editor/AfternoteEditorFormMapper.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/editor/AfternoteEditorFormMapper.kt @@ -119,7 +119,7 @@ internal object AfternoteEditorFormMapper { memorialThumbnailUrl: String? = null, ): MemorialWritePayload = MemorialWritePayload( - memorialPhotoUrl = memorialPhotoUrl?.ifBlank { null }, + memorialPhotoUrl = memorialPhotoUrl, songs = playlistSongs.map { it.toMemorialSongPayload() }, memorialVideo = memorialVideoPayload(videoUrl = memorialVideoUrl, thumbnailUrl = memorialThumbnailUrl), ) @@ -368,7 +368,7 @@ internal object AfternoteEditorFormMapper { MemorialSnapshot( title = title, leaveMessageBlocks = leaveMessageBlocks, - photoUrl = content.media.photoUrl?.ifBlank { null }, + photoUrl = content.media.photoUrl, video = content.media.toVideoPayload(), songs = content.songs.map { song -> @@ -428,7 +428,7 @@ internal object AfternoteEditorFormMapper { MemorialSnapshot( title = title, leaveMessageBlocks = leaveMessageBlocks, - photoUrl = memorialMediaUrls.memorialPhotoUrl?.ifBlank { null }, + photoUrl = memorialMediaUrls.memorialPhotoUrl, video = memorialMediaUrls.toVideoPayload(), songs = playlistSongs.map { it.toMemorialSongPayload() }, ) @@ -455,14 +455,14 @@ internal object AfternoteEditorFormMapper { private fun MemorialMediaUrls.toVideoPayload(): MemorialVideoPayload? = memorialVideoPayload(videoUrl = memorialVideoUrl, thumbnailUrl = memorialThumbnailUrl) - /** 영상 주소가 비었으면 영상 슬롯 자체가 없다. 썸네일만 남은 값은 싣지 않는다. */ + /** + * 영상 주소가 없으면 영상 슬롯 자체가 없다. 썸네일만 남은 값은 싣지 않는다. + * 빈 문자열은 입구(상세 data 매퍼, 폼의 `MediaInput` 판정과 영상 첨부)에서 이미 `null` 로 맞춰져 들어온다. + */ private fun memorialVideoPayload( videoUrl: String?, thumbnailUrl: String?, - ): MemorialVideoPayload? = - videoUrl?.ifBlank { null }?.let { url -> - MemorialVideoPayload(videoUrl = url, thumbnailUrl = thumbnailUrl?.ifBlank { null }) - } + ): MemorialVideoPayload? = videoUrl?.let { url -> MemorialVideoPayload(videoUrl = url, thumbnailUrl = thumbnailUrl) } } /** diff --git a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/editor/state/EditableMemorialVideo.kt b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/editor/state/EditableMemorialVideo.kt index 1097c2836a..9c7cbfcba1 100644 --- a/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/editor/state/EditableMemorialVideo.kt +++ b/feature/afternote/presentation/src/main/kotlin/com/afternote/feature/afternote/presentation/editor/state/EditableMemorialVideo.kt @@ -59,12 +59,12 @@ internal sealed class EditableMemorialVideo { /** 새 선택으로 교체한다. 이전 선택의 썸네일은 물려주지 않는다. 빈 문자열은 [MemorialVideoAttachment.ofOrNull] 규칙대로 첨부 없음이다. */ internal fun withSelection(url: String): EditableMemorialVideo = MemorialVideoAttachment.ofOrNull(url)?.let(::PendingUpload) ?: NoVideo - /** 미저장 선택에서 파생된 썸네일만 갱신한다. 선택이 사라졌다면 늦은 결과를 버린다. */ + /** 미저장 선택에서 파생된 썸네일만 갱신한다. 선택이 사라졌다면 늦은 결과를 버린다. 빈 문자열은 썸네일 없음이다. */ internal fun withSelectionThumbnail(url: String): EditableMemorialVideo = when (this) { NoVideo -> this is Uploaded -> this - is PendingUpload -> PendingUpload(video.copy(thumbnailUrl = url)) + is PendingUpload -> PendingUpload(video.copy(thumbnailUrl = url.ifBlank { null })) } /** diff --git a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/editor/state/EditableMemorialVideoTest.kt b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/editor/state/EditableMemorialVideoTest.kt index 25387a4038..df8af4a72b 100644 --- a/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/editor/state/EditableMemorialVideoTest.kt +++ b/feature/afternote/presentation/src/test/kotlin/com/afternote/feature/afternote/presentation/editor/state/EditableMemorialVideoTest.kt @@ -125,6 +125,17 @@ class EditableMemorialVideoTest { assertEquals(serverVideo, afterLateThumbnail.displayed) } + @Test + fun `빈 썸네일 결과는 썸네일 없음이다`() { + val video = + EditableMemorialVideo + .empty() + .withSelection(pickedVideo.url) + .withSelectionThumbnail("") + + assertEquals(MemorialVideoAttachment(url = pickedVideo.url, thumbnailUrl = null), video.displayed) + } + @Test fun `썸네일을 떼도 출처는 남는다`() { assertEquals(