diff --git a/.github/ci-expected-failures.json b/.github/ci-expected-failures.json index 9972fcdee7..f931a54b1a 100644 --- a/.github/ci-expected-failures.json +++ b/.github/ci-expected-failures.json @@ -1,22 +1,6 @@ { "schemaVersion": 1, - "unitTests": [ - { - "task": ":feature:timeletter:data:testDebugUnitTest", - "className": "com.afternote.feature.timeletter.data.dto.TimeLetterDtoContractTest", - "tests": [ - "발신 응답의 명시적 null과 빈 배열은 정상 파싱된다", - "발신 응답에는 존재하지 않는 deliveredAt 없이 정상 파싱된다", - "수신 응답의 기본값 제거 대상 키가 빠지면 실패한다", - "수신 응답의 조건부 null과 공개 전 빈 blocks는 정상 파싱된다" - ], - "issues": [ - 790, - 873 - ], - "reason": "타임레터 DTO 누락 키 계약 게이트 — 구현 머지 전까지 의도된 실패" - } - ], + "unitTests": [], "androidTests": [], "screenshotModules": [] } diff --git a/.github/scripts/ci-expected-failures.mjs b/.github/scripts/ci-expected-failures.mjs index 97ef17bfef..70ad349e76 100644 --- a/.github/scripts/ci-expected-failures.mjs +++ b/.github/scripts/ci-expected-failures.mjs @@ -34,6 +34,11 @@ export function validateExpectedFailuresConfig(config) { if (!config || typeof config !== "object" || Array.isArray(config)) { throw new Error("기대 실패 목록 최상위 값은 객체여야 합니다."); } + // 파일이 사고로 `{}`(또는 다른 스키마)로 대체돼도 이후 루프는 빈 배열을 순회할 뿐이라 + // 조용히 통과한다 — schemaVersion 을 강제해 "다 닫혀서 0건"과 "사고로 0건"을 구분한다. + if (config.schemaVersion !== 1) { + throw new Error(`지원하지 않는 기대 실패 목록 schemaVersion 입니다: ${config.schemaVersion}`); + } for (const [index, entry] of (config.unitTests ?? []).entries()) { const label = `unitTests[${index}]`; requireNonEmptyString(entry?.task, `${label}.task`); diff --git a/.github/scripts/ci-expected-failures.test.mjs b/.github/scripts/ci-expected-failures.test.mjs index 0dec8a3589..d7cced8530 100644 --- a/.github/scripts/ci-expected-failures.test.mjs +++ b/.github/scripts/ci-expected-failures.test.mjs @@ -81,29 +81,52 @@ test("저장소의 기대 실패 목록은 스키마를 지키고 추적 이슈 assert.ok(entry.issues.length > 0); assert.ok(entry.reason.length > 0); } - assert.ok((config.unitTests?.length ?? 0) + (config.screenshotModules?.length ?? 0) > 0); + // 목록이 전부 비는 것 자체는 정상 상태다(모든 기대 실패 게이트가 닫힘) — 파일이 사고로 + // 비워지는 사고는 schemaVersion 강제(아래 스키마 위반 테스트)가 잡는다. }); test("스키마 위반은 로드 시점에 거부된다", () => { assert.throws(() => validateExpectedFailuresConfig(null), /객체여야/); + assert.throws(() => validateExpectedFailuresConfig({}), /schemaVersion/); + assert.throws(() => validateExpectedFailuresConfig({ schemaVersion: 2 }), /schemaVersion/); assert.throws( - () => validateExpectedFailuresConfig({ unitTests: [{ ...sampleConfig.unitTests[0], task: ":feature:x:assemble" }] }), + () => + validateExpectedFailuresConfig({ + schemaVersion: 1, + unitTests: [{ ...sampleConfig.unitTests[0], task: ":feature:x:assemble" }], + }), /testDebugUnitTest/, ); assert.throws( - () => validateExpectedFailuresConfig({ unitTests: [{ ...sampleConfig.unitTests[0], tests: [] }] }), + () => + validateExpectedFailuresConfig({ + schemaVersion: 1, + unitTests: [{ ...sampleConfig.unitTests[0], tests: [] }], + }), /tests/, ); assert.throws( - () => validateExpectedFailuresConfig({ unitTests: [{ ...sampleConfig.unitTests[0], issues: [] }] }), + () => + validateExpectedFailuresConfig({ + schemaVersion: 1, + unitTests: [{ ...sampleConfig.unitTests[0], issues: [] }], + }), /issues/, ); assert.throws( - () => validateExpectedFailuresConfig({ screenshotModules: [{ module: "feature:setting", issues: [1], reason: "r" }] }), + () => + validateExpectedFailuresConfig({ + schemaVersion: 1, + screenshotModules: [{ module: "feature:setting", issues: [1], reason: "r" }], + }), /project path/, ); assert.throws( - () => validateExpectedFailuresConfig({ screenshotModules: [{ module: ":feature:setting:presentation", issues: [1], reason: " " }] }), + () => + validateExpectedFailuresConfig({ + schemaVersion: 1, + screenshotModules: [{ module: ":feature:setting:presentation", issues: [1], reason: " " }], + }), /reason/, ); }); @@ -243,19 +266,35 @@ test("기대 실패 목록·스크립트 변경은 full validation 을 강제한 test("androidTest 스키마: FQCN·테스트 목록·추적 이슈가 강제된다", () => { assert.throws( - () => validateExpectedFailuresConfig({ androidTests: [{ ...sampleConfig.androidTests[0], className: "not a class" }] }), + () => + validateExpectedFailuresConfig({ + schemaVersion: 1, + androidTests: [{ ...sampleConfig.androidTests[0], className: "not a class" }], + }), /FQCN/, ); assert.throws( - () => validateExpectedFailuresConfig({ androidTests: [{ ...sampleConfig.androidTests[0], tests: [] }] }), + () => + validateExpectedFailuresConfig({ + schemaVersion: 1, + androidTests: [{ ...sampleConfig.androidTests[0], tests: [] }], + }), /비어 있지 않은 배열/, ); assert.throws( - () => validateExpectedFailuresConfig({ androidTests: [{ ...sampleConfig.androidTests[0], issues: [] }] }), + () => + validateExpectedFailuresConfig({ + schemaVersion: 1, + androidTests: [{ ...sampleConfig.androidTests[0], issues: [] }], + }), /추적 이슈/, ); assert.throws( - () => validateExpectedFailuresConfig({ androidTests: [{ ...sampleConfig.androidTests[0], devices: [] }] }), + () => + validateExpectedFailuresConfig({ + schemaVersion: 1, + androidTests: [{ ...sampleConfig.androidTests[0], devices: [] }], + }), /비어 있지 않은 배열/, ); assert.doesNotThrow(() => validateExpectedFailuresConfig(sampleConfig)); diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt index 6cc7a00690..90c7f7e36d 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt @@ -724,7 +724,6 @@ private fun completionCreatedLetter(call: FakeTimeLetterRepository.CreateCall): id = 901L, title = call.title, sendAt = call.sendAt, - deliveredAt = null, status = call.status, blocks = emptyList(), receiverIds = call.receiverIds, @@ -751,7 +750,6 @@ private fun completionDetailLetter(): TimeLetter = id = 509L, title = "상세 route 편지", sendAt = "2026-10-09T09:40:00", - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = listOf( diff --git a/feature/timeletter/data/src/debug/kotlin/com/afternote/feature/timeletter/data/network/TimeLetterMockFixtures.kt b/feature/timeletter/data/src/debug/kotlin/com/afternote/feature/timeletter/data/network/TimeLetterMockFixtures.kt index cb55152d90..a91290572e 100644 --- a/feature/timeletter/data/src/debug/kotlin/com/afternote/feature/timeletter/data/network/TimeLetterMockFixtures.kt +++ b/feature/timeletter/data/src/debug/kotlin/com/afternote/feature/timeletter/data/network/TimeLetterMockFixtures.kt @@ -5,8 +5,8 @@ internal object TimeLetterMockFixtures { """ {"status":200,"code":0,"message":null,"data":{ "timeLetters":[ - {"id":1,"title":"미래의 나에게","sendAt":"2030-01-01T00:00:00","deliveredAt":null,"status":"SCHEDULED","blocks":[{"id":1,"blockType":"TEXT","blockOrder":1,"textContent":"지금 이 순간을 잊지 마."}],"receiverIds":[1]}, - {"id":2,"title":"10년 후의 나에게","sendAt":"2035-06-15T00:00:00","deliveredAt":null,"status":"SCHEDULED","blocks":[{"id":2,"blockType":"TEXT","blockOrder":1,"textContent":"지금보다 더 행복하길 바라."}],"receiverIds":[2]} + {"id":1,"title":"미래의 나에게","sendAt":"2030-01-01T00:00:00","status":"SCHEDULED","blocks":[{"id":1,"blockType":"TEXT","blockOrder":1,"textContent":"지금 이 순간을 잊지 마.","url":null,"mimeType":null}],"receiverIds":[1]}, + {"id":2,"title":"10년 후의 나에게","sendAt":"2035-06-15T00:00:00","status":"SCHEDULED","blocks":[{"id":2,"blockType":"TEXT","blockOrder":1,"textContent":"지금보다 더 행복하길 바라.","url":null,"mimeType":null}],"receiverIds":[2]} ], "totalCount":2 }} @@ -16,7 +16,7 @@ internal object TimeLetterMockFixtures { """ {"status":200,"code":0,"message":null,"data":{ "timeLetters":[ - {"id":3,"title":"임시저장 레터","sendAt":null,"deliveredAt":null,"status":"DRAFT","blocks":[],"receiverIds":[]} + {"id":3,"title":"임시저장 레터","sendAt":null,"status":"DRAFT","blocks":[],"receiverIds":[]} ], "totalCount":1 }} @@ -24,12 +24,12 @@ internal object TimeLetterMockFixtures { val MOCK_CREATE_JSON = """ - {"status":200,"code":0,"message":null,"data":{"id":99,"title":"새 타임레터","sendAt":null,"deliveredAt":null,"status":"DRAFT","blocks":[],"receiverIds":[]}} + {"status":200,"code":0,"message":null,"data":{"id":99,"title":"새 타임레터","sendAt":null,"status":"DRAFT","blocks":[],"receiverIds":[]}} """.trimIndent() val MOCK_UPDATE_JSON = """ - {"status":200,"code":0,"message":null,"data":{"id":99,"title":"수정된 타임레터","sendAt":"2030-01-01T00:00:00","deliveredAt":null,"status":"SCHEDULED","blocks":[{"id":1,"blockType":"TEXT","blockOrder":1,"textContent":"수정된 내용"}],"receiverIds":[1]}} + {"status":200,"code":0,"message":null,"data":{"id":99,"title":"수정된 타임레터","sendAt":"2030-01-01T00:00:00","status":"SCHEDULED","blocks":[{"id":1,"blockType":"TEXT","blockOrder":1,"textContent":"수정된 내용","url":null,"mimeType":null}],"receiverIds":[1]}} """.trimIndent() val MOCK_DELETE_JSON = @@ -39,6 +39,6 @@ internal object TimeLetterMockFixtures { fun detailJson(id: Long): String = """ - {"status":200,"code":0,"message":null,"data":{"id":$id,"title":"타임레터 #$id","sendAt":"2030-01-01T00:00:00","deliveredAt":null,"status":"SCHEDULED","blocks":[{"id":1,"blockType":"TEXT","blockOrder":1,"textContent":"내용입니다."}],"receiverIds":[1]}} + {"status":200,"code":0,"message":null,"data":{"id":$id,"title":"타임레터 #$id","sendAt":"2030-01-01T00:00:00","status":"SCHEDULED","blocks":[{"id":1,"blockType":"TEXT","blockOrder":1,"textContent":"내용입니다.","url":null,"mimeType":null}],"receiverIds":[1]}} """.trimIndent() } diff --git a/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/dto/ReceivedTimeLetterDto.kt b/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/dto/ReceivedTimeLetterDto.kt index a89774872c..f206ad1e6f 100644 --- a/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/dto/ReceivedTimeLetterDto.kt +++ b/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/dto/ReceivedTimeLetterDto.kt @@ -7,14 +7,15 @@ import kotlinx.serialization.Serializable data class ReceivedTimeLetterDto( @SerialName("id") val id: Long, @SerialName("timeLetterReceiverId") val timeLetterReceiverId: Long, - @SerialName("title") val title: String? = null, - @SerialName("blocks") val blocks: List = emptyList(), - @SerialName("sendAt") val sendAt: String? = null, + @SerialName("title") val title: String?, + @SerialName("blocks") val blocks: List, + @SerialName("sendAt") val sendAt: String?, @SerialName("status") val status: TimeLetterStatusDto, - @SerialName("senderName") val senderName: String? = null, - @SerialName("deliveredAt") val deliveredAt: String? = null, - @SerialName("createdAt") val createdAt: String? = null, - @SerialName("isRead") val isRead: Boolean, + @SerialName("senderName") val senderName: String?, + @SerialName("deliveredAt") val deliveredAt: String?, + @SerialName("createdAt") val createdAt: String?, + // 공개 전 미리보기에선 읽음 여부가 아직 정해지지 않아 서버가 명시적 null을 보낼 수 있다(#790). + @SerialName("isRead") val isRead: Boolean?, ) @Serializable diff --git a/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/dto/TimeLetterDto.kt b/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/dto/TimeLetterDto.kt index 7cb0c18803..7264cad0e2 100644 --- a/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/dto/TimeLetterDto.kt +++ b/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/dto/TimeLetterDto.kt @@ -85,12 +85,13 @@ data class TimeLetterDeleteRequestDto( @SerialName("timeLetterIds") val timeLetterIds: List, ) +// deliveredAt 필드가 없다 — TimeLetterResponse(발신 응답) 계약엔 이 키가 아예 없다(#790). +// 배달 시각은 ReceivedTimeLetterDto 쪽(수신 응답)에만 존재한다. @Serializable data class TimeLetterDto( @SerialName("id") val id: Long, @SerialName("title") val title: String?, @SerialName("sendAt") val sendAt: String?, - @SerialName("deliveredAt") val deliveredAt: String?, @SerialName("status") val status: TimeLetterStatusDto, @SerialName("blocks") val blocks: List, @SerialName("receiverIds") val receiverIds: List, diff --git a/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/mapper/ReceivedTimeLetterMapper.kt b/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/mapper/ReceivedTimeLetterMapper.kt index 7c5a756beb..2a50034692 100644 --- a/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/mapper/ReceivedTimeLetterMapper.kt +++ b/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/mapper/ReceivedTimeLetterMapper.kt @@ -16,7 +16,9 @@ fun ReceivedTimeLetterDto.toDomain(): ReceivedTimeLetter = senderName = senderName, deliveredAt = deliveredAt, createdAt = createdAt, - isRead = isRead, + // 공개 전엔 서버가 isRead를 null로 보낼 수 있다 — 도메인은 non-null 계약이라 미확정 상태를 + // 안전한 쪽(false=안 읽음)으로 접어 화면이 잘못 "읽음"으로 표시되지 않게 한다. + isRead = isRead ?: false, ) fun ReceivedTimeLetterListDto.toDomain(): ReceivedTimeLetterList = diff --git a/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/mapper/TimeLetterMapper.kt b/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/mapper/TimeLetterMapper.kt index 24b6a37ce3..dba509e940 100644 --- a/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/mapper/TimeLetterMapper.kt +++ b/feature/timeletter/data/src/main/kotlin/com/afternote/feature/timeletter/data/mapper/TimeLetterMapper.kt @@ -59,7 +59,6 @@ fun TimeLetterDto.toDomain(): TimeLetter = id = id, title = title, sendAt = sendAt, - deliveredAt = deliveredAt, status = status.toDomain(), blocks = blocks.map { it.toDomain() }, receiverIds = receiverIds, diff --git a/feature/timeletter/domain/src/main/kotlin/com/afternote/feature/timeletter/domain/model/TimeLetterModel.kt b/feature/timeletter/domain/src/main/kotlin/com/afternote/feature/timeletter/domain/model/TimeLetterModel.kt index 271f1210e3..b3b302e92b 100644 --- a/feature/timeletter/domain/src/main/kotlin/com/afternote/feature/timeletter/domain/model/TimeLetterModel.kt +++ b/feature/timeletter/domain/src/main/kotlin/com/afternote/feature/timeletter/domain/model/TimeLetterModel.kt @@ -4,7 +4,6 @@ data class TimeLetter( val id: Long, val title: String?, val sendAt: String?, - val deliveredAt: String?, val status: TimeLetterStatus, val blocks: List, val receiverIds: List, diff --git a/feature/timeletter/domain/src/test/kotlin/com/afternote/feature/timeletter/domain/testing/FakeTimeLetterRepositoryTest.kt b/feature/timeletter/domain/src/test/kotlin/com/afternote/feature/timeletter/domain/testing/FakeTimeLetterRepositoryTest.kt index 935aa8c3f3..8de26bc64f 100644 --- a/feature/timeletter/domain/src/test/kotlin/com/afternote/feature/timeletter/domain/testing/FakeTimeLetterRepositoryTest.kt +++ b/feature/timeletter/domain/src/test/kotlin/com/afternote/feature/timeletter/domain/testing/FakeTimeLetterRepositoryTest.kt @@ -54,7 +54,6 @@ class FakeTimeLetterRepositoryTest { id = 1L, title = "편지", sendAt = null, - deliveredAt = null, status = status, blocks = emptyList(), receiverIds = listOf(7L), diff --git a/feature/timeletter/domain/src/testFixtures/kotlin/com/afternote/feature/timeletter/domain/testing/FakeTimeLetterRepository.kt b/feature/timeletter/domain/src/testFixtures/kotlin/com/afternote/feature/timeletter/domain/testing/FakeTimeLetterRepository.kt index 1055a4f0f0..5ef38106b4 100644 --- a/feature/timeletter/domain/src/testFixtures/kotlin/com/afternote/feature/timeletter/domain/testing/FakeTimeLetterRepository.kt +++ b/feature/timeletter/domain/src/testFixtures/kotlin/com/afternote/feature/timeletter/domain/testing/FakeTimeLetterRepository.kt @@ -115,7 +115,6 @@ class FakeTimeLetterRepository( id = id, title = title, sendAt = sendAt, - deliveredAt = null, status = status, blocks = blocks.mapIndexed { index, block -> block.toStoredBlock(id, index) }, receiverIds = receiverIds, diff --git a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/DraftLetterItem.kt b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/DraftLetterItem.kt index faef0f9d61..71e95357c2 100644 --- a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/DraftLetterItem.kt +++ b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/DraftLetterItem.kt @@ -129,7 +129,6 @@ private fun DraftLetterItemPreview() { id = 1L, title = "미래의 나에게", sendAt = "2026-12-31T00:00:00", - deliveredAt = null, status = TimeLetterStatus.DRAFT, blocks = emptyList(), receiverIds = listOf(1L), diff --git a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeLetterBlockItem.kt b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeLetterBlockItem.kt index 554fd5cd0c..a0d2781030 100644 --- a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeLetterBlockItem.kt +++ b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeLetterBlockItem.kt @@ -148,7 +148,6 @@ private fun TimeLetterBlockItemPrev() { id = 1L, title = "미래의 나에게", sendAt = "2026-12-31T00:00:00", - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = emptyList(), receiverIds = listOf(1L), diff --git a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeLetterContent.kt b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeLetterContent.kt index 5d84fa96ab..7c87447179 100644 --- a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeLetterContent.kt +++ b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeLetterContent.kt @@ -151,7 +151,6 @@ private val previewLetters = id = 1L, title = "미래의 나에게", sendAt = "2026-12-31T00:00:00", - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = emptyList(), receiverIds = listOf(1L), @@ -160,7 +159,6 @@ private val previewLetters = id = 2L, title = "10년 후의 나에게", sendAt = "2035-01-01T00:00:00", - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = emptyList(), receiverIds = listOf(2L), diff --git a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeletterListItem.kt b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeletterListItem.kt index 475e8da43c..f0e0472bc6 100644 --- a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeletterListItem.kt +++ b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/component/TimeletterListItem.kt @@ -141,7 +141,6 @@ private fun TimeLetterItemPreview() { id = 1L, title = "미래의 나에게", sendAt = "2026-12-31T00:00:00", - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = emptyList(), receiverIds = listOf(1L), diff --git a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/recipient/RecipientTimeletterScreen.kt b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/recipient/RecipientTimeletterScreen.kt index 17520ffb1a..d972931609 100644 --- a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/recipient/RecipientTimeletterScreen.kt +++ b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/recipient/RecipientTimeletterScreen.kt @@ -228,7 +228,6 @@ private fun ReceivedTimeLetter.toTimeLetter() = id = id, title = title, sendAt = deliveredAt, - deliveredAt = deliveredAt, status = status, blocks = blocks, receiverIds = listOf(id), diff --git a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/DraftLetterScreen.kt b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/DraftLetterScreen.kt index 46e3af064a..1eca58253b 100644 --- a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/DraftLetterScreen.kt +++ b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/DraftLetterScreen.kt @@ -194,8 +194,30 @@ private fun DraftLetterScreenPreview() { DraftLetterUiState.Success( drafts = listOf( - TimeLetter(1L, "첫 번째 레터", "2026-12-25T00:00:00", null, TimeLetterStatus.DRAFT, emptyList(), listOf(1L)), - TimeLetter(2L, "두 번째 레터", null, null, TimeLetterStatus.DRAFT, emptyList(), listOf(2L)), + TimeLetter( + id = 1L, + title = "첫 번째 레터", + sendAt = "2026-12-25T00:00:00", + status = TimeLetterStatus.DRAFT, + blocks = emptyList(), + receiverIds = listOf(1L), + ), + TimeLetter( + id = 2L, + title = "두 번째 레터", + sendAt = "2026-06-01T00:00:00", + status = TimeLetterStatus.DRAFT, + blocks = emptyList(), + receiverIds = listOf(2L), + ), + TimeLetter( + id = 3L, + title = null, + sendAt = null, + status = TimeLetterStatus.DRAFT, + blocks = emptyList(), + receiverIds = emptyList(), + ), ), receiverNameMap = mapOf(1L to "김지은", 2L to "이현우"), ), @@ -207,3 +229,40 @@ private fun DraftLetterScreenPreview() { onDeleteSelected = {}, ) } + +@Preview(showBackground = true, name = "수정 모드 - 항목 선택됨") +@Composable +private fun DraftLetterScreenEditModePreview() { + DraftLetterContent( + uiState = + DraftLetterUiState.Success( + drafts = + listOf( + TimeLetter( + id = 1L, + title = "첫 번째 레터", + sendAt = "2026-12-25T00:00:00", + status = TimeLetterStatus.DRAFT, + blocks = emptyList(), + receiverIds = listOf(1L), + ), + TimeLetter( + id = 2L, + title = "두 번째 레터", + sendAt = "2026-06-01T00:00:00", + status = TimeLetterStatus.DRAFT, + blocks = emptyList(), + receiverIds = listOf(2L), + ), + ), + isEditMode = true, + selectedIds = setOf(1L), + ), + onBackClick = {}, + onOpenDraft = {}, + onEditCompleteClick = {}, + onToggleSelection = {}, + onDeleteAll = {}, + onDeleteSelected = {}, + ) +} diff --git a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeLetterDetailScreen.kt b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeLetterDetailScreen.kt index 6718339c00..459968a06b 100644 --- a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeLetterDetailScreen.kt +++ b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeLetterDetailScreen.kt @@ -284,7 +284,6 @@ private fun TimeLetterDetailScreenPrev() { id = 1L, title = "미래의 나에게", sendAt = "2026-12-31T00:00:00", - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = listOf( diff --git a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeletterScreen.kt b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeletterScreen.kt index 228f09a694..e4bede3e23 100644 --- a/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeletterScreen.kt +++ b/feature/timeletter/presentation/src/main/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeletterScreen.kt @@ -98,7 +98,6 @@ private val previewLetters = id = 1L, title = "미래의 나에게", sendAt = "2026-12-31T00:00:00", - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = emptyList(), receiverIds = listOf(1L), @@ -107,7 +106,6 @@ private val previewLetters = id = 2L, title = "10년 후의 나에게", sendAt = "2035-01-01T00:00:00", - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = emptyList(), receiverIds = listOf(2L), diff --git a/feature/timeletter/presentation/src/screenshotTest/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeletterScreenScreenshotTest.kt b/feature/timeletter/presentation/src/screenshotTest/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeletterScreenScreenshotTest.kt index 8cfb978a22..668bda0ad5 100644 --- a/feature/timeletter/presentation/src/screenshotTest/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeletterScreenScreenshotTest.kt +++ b/feature/timeletter/presentation/src/screenshotTest/kotlin/com/afternote/feature/timeletter/presentation/screen/sender/TimeletterScreenScreenshotTest.kt @@ -86,7 +86,6 @@ private val previewLetters = id = 1L, title = "미래의 나에게", sendAt = "2026-12-31T00:00:00", - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = emptyList(), receiverIds = listOf(1L), @@ -95,7 +94,6 @@ private val previewLetters = id = 2L, title = "10년 후의 나에게", sendAt = "2035-01-01T00:00:00", - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = emptyList(), receiverIds = listOf(2L), diff --git a/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/TimeLetterFlowTest.kt b/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/TimeLetterFlowTest.kt index 0e8b2100bf..8db52aeee4 100644 --- a/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/TimeLetterFlowTest.kt +++ b/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/TimeLetterFlowTest.kt @@ -74,7 +74,6 @@ class TimeLetterFlowTest { id = 1L, title = call.title, sendAt = call.sendAt, - deliveredAt = null, status = call.status, blocks = emptyList(), receiverIds = call.receiverIds, diff --git a/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/TimeLetterLifecycleTest.kt b/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/TimeLetterLifecycleTest.kt index ce5ca0c119..35fe45a17f 100644 --- a/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/TimeLetterLifecycleTest.kt +++ b/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/TimeLetterLifecycleTest.kt @@ -587,7 +587,6 @@ private fun timeLetter( id = id, title = title, sendAt = sendAt, - deliveredAt = null, status = status, blocks = blocks, receiverIds = receiverIds, diff --git a/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/DraftLetterViewModelTest.kt b/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/DraftLetterViewModelTest.kt index 375d1c5cd5..43a2e86603 100644 --- a/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/DraftLetterViewModelTest.kt +++ b/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/DraftLetterViewModelTest.kt @@ -176,7 +176,6 @@ class DraftLetterViewModelTest { id = 1L, title = "draft", sendAt = null, - deliveredAt = null, status = TimeLetterStatus.DRAFT, blocks = emptyList(), receiverIds = listOf(1L), diff --git a/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/TimeLetterWriteViewModelTest.kt b/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/TimeLetterWriteViewModelTest.kt index d62a1254e9..c9d58600c2 100644 --- a/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/TimeLetterWriteViewModelTest.kt +++ b/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/TimeLetterWriteViewModelTest.kt @@ -213,7 +213,6 @@ class TimeLetterWriteViewModelTest { id = 10L, title = "existing title", sendAt = "2026-08-29T19:30:00", - deliveredAt = null, status = status, blocks = emptyList(), receiverIds = listOf(1L), diff --git a/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/TimeletterViewModelTest.kt b/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/TimeletterViewModelTest.kt index d1f54f36dc..c144434c64 100644 --- a/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/TimeletterViewModelTest.kt +++ b/feature/timeletter/presentation/src/test/kotlin/com/afternote/feature/timeletter/presentation/viewmodel/TimeletterViewModelTest.kt @@ -137,7 +137,6 @@ class TimeletterViewModelTest { id = 1L, title = "제목", sendAt = null, - deliveredAt = null, status = TimeLetterStatus.SCHEDULED, blocks = emptyList(), receiverIds = emptyList(),