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 020b5e858..0c7f247ce 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 @@ -185,11 +185,15 @@ private fun AfternoteMemorialDto.toDraftMemorialContent() = media = toMemorialMedia(), ) +/** + * 미디어 URL 의 빈 값은 여기서 한 번만 `null` 로 맞춘다. 앱 안에서는 `null` 만 「없음」이다. + * BE `main` 은 빈 미디어를 `null` 로 저장하므로 빈 문자열은 옛 데이터에서만 온다. + */ private fun AfternoteMemorialDto.toMemorialMedia() = MemorialMedia( - photoUrl = memorialPhotoUrl, - videoUrl = memorialVideo?.videoUrl, - thumbnailUrl = memorialVideo?.thumbnailUrl, + photoUrl = memorialPhotoUrl?.ifBlank { null }, + videoUrl = memorialVideo?.videoUrl?.ifBlank { null }, + thumbnailUrl = 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 5040d7cc9..afb4f1b25 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 99997928a..1bb1f767b 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 1097c2836..9c7cbfcba 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 25387a403..df8af4a72 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(