Skip to content

DTO 기본값·receiverIds nullable 제거 — 누락 계약을 빈 값으로 은폐 #790

Closed

Description

@OnlyReadOneBook

📜 Overview (Required)

origin/develop 62d5935c와 박경민 담당의 열린 PR #763·#767을 기준으로 feature/timeletter·feature/setting의 DTO 기본 파라미터와 전 계층 nullable을 전수 확인했다.

타임레터 DTO는 서버가 항상 키를 직렬화하고 모든 생성 호출부가 인자를 전달하는데도 = null·= emptyList()가 광범위하게 들어가 있다. 그 결과 키 누락이 명시적 null·빈 블록·빈 수신자 목록과 구분되지 않는다. 생성 경로의 receiverIds는 UI의 빈 목록을 굳이 null로 바꿨다가 repository에서 다시 빈 목록으로 바꾸고 있어 nullable 상태가 아무 의미도 갖지 않는다.

확인된 대상

  1. DTO의 불필요한 기본 파라미터 제거

  2. DTO 밖까지 전파된 불필요한 receiverIds nullable 제거

    • TimeLetterWriteViewModel의 state.recipientIds.ifEmpty { null }
    • CreateTimeLetterUseCase.receiverIds: List<Long>?
    • TimeLetterRepository.createTimeLetter(... receiverIds: List<Long>?) 및 구현체
    • 구현체의 receiverIds ?: emptyList()

    생성 API는 수신자 목록을 필수로 검증하고, FE 내부에서도 null은 즉시 빈 목록으로 합쳐진다. 전 경계를 List<Long>으로 좁히고 state.recipientIds를 그대로 전달한다. 수신자 미선택 허용 여부와 오류 문구는 #570의 제품 동작 문제이며, nullable 타입으로 우회할 문제가 아니다.

  3. 유지할 nullable

    • 임시저장 제목·발송일, 발송 전 deliveredAt, 블록 종류에 따른 textContent/url/mimeType, 공개 전 수신 레터의 제목·발신자·작성 시각은 실제 계약상 조건부이므로 nullable을 유지한다.
    • feature/setting에는 DTO가 없고, 확인된 nullable은 미선택 관계·선택 날짜·오류·연결 전 계정 정보처럼 실제 UI 상태를 나타내므로 제거 대상이 없다.

완료 기준

  • 두 타임레터 DTO 파일에서 키 생략을 허용할 근거가 없는 기본 파라미터를 제거한다.
  • 요청 mapper/repository는 nullable 값을 포함해 모든 생성 인자를 명시한다.
  • 응답 키 누락은 명시적 null·빈 배열로 성공하지 않고 계약 테스트가 실패한다.
  • 생성 receiverIds는 ViewModel → use case → repository → request DTO까지 non-null 목록으로 이어진다.
  • 조건부 nullable의 명시적 null과 공개 전 빈 blocks는 정상 역직렬화되는 테스트를 유지한다.
  • PR fix(timeletter) : 타임레터 API 동기화 및 임시저장 오류 처리 (#570, #568, #428, #693) [base: develop] #763·#767의 공통 변경도 같은 기준으로 정리한다.

📌 Child Issue(Optional)

No response

📍 Note (Optional)

Activity

  1. OnlyReadOneBook commented on Aug 23, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    #932 를 실측 대조하면서 이 이슈 범위에 얹으면 추가 조사 없이 끝나는 일 하나를 확인해 옮깁니다.

    응답 DTO 15개 파일 중 어느 엔드포인트의 응답인지 · 그 판정 근거가 무엇인지 표기가 없는 파일이 8개인데,
    그중 2개가 이 이슈가 이미 손대는 파일입니다.

    이 이슈에서 "서버가 이 키를 항상 보내는가" 를 필드별로 판정하게 되는데, 그 판정 결과를 KDoc 으로 같이 남겨 주시면
    됩니다. 판정을 새로 해야 하는 일이 아니라 이미 하는 판정을 코드에 적어 두는 것뿐입니다.

    양식은 mindrecord 가 이미 쓰고 있는 것을 그대로 따르면 됩니다 —
    DiaryDto.kt 참조.

    /**
     * `GET /diary` 목록 항목 (Swagger `DiaryResponse` 실측, 2026-08-03).
     *
     * 식별자를 뺀 나머지에 기본값을 두는 이유는 실패의 폭이다. 이 DTO 는 목록 원소라 ...
     */

    별도 이슈로 찢지 않고 여기로 넘기는 이유는, 같은 두 파일을 두 이슈가 각각 열면 충돌이 나고
    근거 판정이 이 이슈 안에서 이미 이뤄지기 때문입니다. #932 에는 koongmai 몫으로 이 항목을 적어 뒀습니다.

  2. OnlyReadOneBook commented on Aug 23, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    #676 을 BE 실코드 대조로 재정의하면서 timeletter 몫 1건이 나왔습니다.

    FE BE record FE 에만 (유령)
    TimeLetterDto TimeLetterResponse deliveredAt

    Afternote-BE 0fb9e0b 의 TimeLetterResponse 에 deliveredAt 이 없습니다. FE 는 String? = null 로 받고 있어
    파싱은 성공하지만 값이 영영 오지 않습니다.

    이 이슈가 "발송 전 deliveredAt" 을 유지할 nullable 로 분류해 두셨는데, BE 가 그 키를 아예 안 보내는 것이라면
    "조건부로 null" 이 아니라 "존재하지 않는 필드" 입니다. 판단이 갈립니다.

    이 이슈에서 기본값을 걷으며 실서버 응답을 확인하실 때 deliveredAt 키의 존재도 같이 봐 주시면 됩니다.

    앞서 드린 계약 근거 주석 2건과 같은 맥락입니다 — 판정 결과를 KDoc 으로 남겨 주시면 #932 쪽 몫도 함께 해소됩니다.

  3. OnlyReadOneBook commented on Aug 23, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    앞서 넘긴 "계약 근거 주석" 몫을 철회합니다. 하지 않으셔도 됩니다.

    엔드포인트는 ApiService 와 Swagger 에, nullable 근거는 BE @Schema(description) 에 이미 있습니다.
    FE 가 KDoc 으로 복제하면 정본이 둘로 갈리고, BE 가 바뀔 때 FE 주석만 낡아 거짓이 됩니다.

    실제로 그렇게 됐습니다 — Detail.kt 에 있던 계약 설명 주석(카테고리별 non-null 표,
    ReceiverRequest 는 이 필드 하나뿐(2026-08-02 Swagger 실측), name·relation·phone 은 실리지 않아 빈 문자열)이
    전부 현재 계약과 어긋나 #676 에서 삭제됐습니다. receivers 는 전 카테고리 공통이고, BE #81 이 닫히며
    name·relation 은 이제 옵니다.

    남길 가치가 있는 주석은 명세를 봐도 모르는 FE 판단뿐입니다 — wire 이름과 도메인 이름이 다른 이유,
    실패 폭 때문에 명세와 달리 기본값을 두는 결정 같은 것. 그건 Swagger 어디에도 없으니까요.

    #932 는 afternote 매퍼 명명 정리만 남기고 축소했습니다.

  4. changed the title [-]refactor(timeletter): DTO 기본값·receiverIds nullable 제거 — 누락 계약을 빈 값으로 은폐[/-] [+]DTO 기본값·receiverIds nullable 제거 — 누락 계약을 빈 값으로 은폐[/+] on Aug 28, 2026
  5. added
    refactor동작 변경 없이 코드 구조·책임·경계를 개선하는 작업
    and removed
    enhancementNew feature or request
    on Aug 28, 2026
  6. added a commit that references this issue on Aug 28, 2026
  7. OnlyReadOneBook commented on Aug 29, 2026

    @OnlyReadOneBook
    CollaboratorAuthor

    #1494 (PR #1495) 에서 전역 coerceInputValues 를 걷어내며 이 이슈 범위에 걸리는 걸 하나 남겨 둡니다 — 담당 모듈이 달라 제 PR 에서 건드리지 않았습니다.

    feature/timeletter/data/src/test/kotlin/.../TimeLetterDtoContractTest.kt:28 의 테스트 픽스처가 아직 프로덕션 설정을 복제합니다.

    private val json = Json {
        ignoreUnknownKeys = true
        coerceInputValues = true   // ← 프로덕션에서는 사라졌습니다
    }

    그대로 두면 이 계약 테스트가 프로덕션에 없는 관용성을 기준으로 검증합니다. core/network 쪽 픽스처 5곳은 #1495 에서 함께 맞췄습니다.

    참고로 이 이슈 작업에 쓸 수 있는 실측 근거입니다.

    • blocks 는 서버가 null 로 보내지 않습니다. BE ReceivedTimeLetterResponse.java 가 미공개 구간에 List.of() 를 채웁니다. 그래서 = emptyList() 기본값이 실제로 가리는 것은 «키 자체가 빠지는» 경우뿐입니다.
    • isRead 는 실제로 null 이 옵니다 — 같은 파일에서 .isRead(isAvailable ? (…getReadAt() != null) : null). 현재 FE 선언은 val isRead: Boolean 이라 기본값이 없어, 플래그가 있던 시절에도 이 응답은 실패했습니다. 이 이슈의 xfail 테스트가 assertNull(decoded.isRead) 로 기대하는 것과 같은 방향입니다.
    • :konsist:test 허용목록에서 이미 해소된 13건은 refactor(core): 전역 coerceInputValues 를 걷어낸다 (#1494) #1495 가 지웠고, ReceivedTimeLetterDto.blocks 만 남겨 뒀습니다. 이 이슈가 해소되면 그 한 줄과 ci-expected-failures.json 항목을 함께 걷으시면 됩니다.
  8. added
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    and removed
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Aug 30, 2026
  9. added
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    and removed
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Sep 5, 2026
  10. removed
    pr-open이 이슈를 구현하는 PR 이 열려 있다 — base 가 기본 브랜치가 아니어도 드러낸다
    on Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:timeletter타임레터 기능 영역refactor동작 변경 없이 코드 구조·책임·경계를 개선하는 작업

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions