Skip to content

refactor(receiver): 수신자 상세 애프터노트 제목의 근거 없는 nullable 을 걷는다 — 도달 불가능한 «제목 없는 화면» #1177

Closed

Description

@OnlyReadOneBook

📜 Overview (Required)

수신자 상세의 애프터노트 제목만 String? 이다. 서버가 비어 있을 수 없게 3중으로 강제하는 값이고, FE 의 다른 모든 애프터노트 제목은 이미 non-null 이다. 이 한 곳의 nullable 이 매퍼의 title.orEmpty() 를 낳아 도달 불가능한 «제목 없는 상세 화면» 을 코드로 만들어 두고 있다.

// ReceivedAfternoteDetailSuccessMapper.kt:34, 44 — 이 orEmpty() 가 그리는 화면은 서버에서 올 수 없다
serviceName = title.orEmpty(),

계약 실측 — 제목은 null 도 빈 문자열도 될 수 없다

근거는 Afternote-BE origin/release 실코드다(2026-08-26 확인). dev 서버는 조회 시각에 닫혀 있었고, 계약은 코드로 확정된다.

# 강제 지점 실코드
1 생성 요청 AfternoteCreateRequest.java:18-21 @NotBlank String title — 컨트롤러 AfternoteController.java:87·102 가 @Valid 로 발효
2 엔티티 Afternote.java:34-35 @Column(nullable = false, length = 100)
3 수정 경로 AfternoteService.java:271 request.getTitle() != null ? request.getTitle() : afternote.getTitle() — null 로 덮이는 경로가 없다

플레이리스트 카테고리도 예외가 아니다 — PlaylistRelationStrategy.java:115 가 atmosphere != null ? atmosphere : DEFAULT_PLAYLIST_TITLE 로 반드시 채운다.

응답은 그 NOT NULL 컬럼 값을 그대로 싣는다. ReceivedAfternoteDetailResponse.java 의 조립부 3곳 전부 —
fromSocial:110 · fromGallery:122 · fromPlaylist:167 이 .title(afternote.getTitle()) 이고, record 의 title 필드에 @Schema(nullable = true) 가 없다. Jackson NON_NULL 설정이 없어 record 에 있으면 키는 항상 나간다(#676 에서 확립된 판정 기준).

임시저장 예외 논쟁 자체가 성립하지 않는다 — AfternoteReceiverRepository.java:20·30 의 조회 쿼리가 AND a.isDraft = false 로 임시저장을 수신자에게서 아예 배제한다.

FE 도 같은 방향으로 이미 막고 있다 — AfternoteSaveState.kt:26 의 TITLE_REQUIRED 가 제목 없는 저장을 에디터에서 거절한다.

FE 안에서 이 한 곳만 어긋나 있다

파일 선언
AfternoteDto.kt 발신자 쪽 애프터노트 제목 7곳 전부 String ✅
ReceiverAfternoteDto.kt:16 ReceivedAfternoteDto.title: String (수신 목록) ✅
ReceiverAfternoteDto.kt:52 ReceivedSongDto.title: String ✅
ReceiverAfternoteDto.kt:28 ReceivedAfternoteDetailDto.title: String? = null 🔴
ReceiverAfternoteModels.kt:23 ReceivedAfternoteDetail.title: String? = null 🔴

같은 파일 안에서 목록은 non-null, 상세만 nullable 이다. 검토된 결정이 아니라 손대지 않은 초기값이다.

할 일

  1. ReceivedAfternoteDetailDto.title → String (기본값 없음). 키가 빠지면 MissingFieldException 으로 시끄럽게 실패한다 — NetworkModule.provideJson 의 coerceInputValues = true 는 기본값이 있어야 동작하므로 이 필드를 조용히 구해주지 않는다.
  2. ReceivedAfternoteDetail.title → String (기본값 없음).
  3. 매퍼의 title.orEmpty() 2곳(ReceivedAfternoteDetailSuccessMapper.kt:34·44) 제거.
  4. 딸려 나오는 테스트 조정 — ReceivedAfternoteDetailMapperTest 의 title 미지정 생성 5곳, LeaveMessageBlockContractTest.kt:104 의 title 키 없는 JSON 픽스처. androidTest 3곳(AppAndReceiverCompletionAndroidTest 2 · ReceiverAdvancedAndroidTest 1)은 이미 title 을 명시하고 있어 영향 없다.
  5. 제목 키가 빠진 응답이 성공하지 않고 실패함을 계약 테스트로 고정한다.

완료 기준

  • ReceivedAfternoteDetailDto·ReceivedAfternoteDetail 의 title 이 non-null 이고 기본값이 없다.
  • 매퍼에 title 폴백이 남아 있지 않다.
  • 서버가 title 키를 빼면 파싱이 성공하지 않는다(테스트로 고정).
  • :app:compileDebugUnitTestKotlin 포함 전 모듈 컴파일 green.

📌 Child Issue(Optional)

No response

📍 Note (Optional)

인접 이슈와의 경계

범위 밖

ReceivedDetailContentUiModel.kt:36·46 의 serviceName: String = "" 등 UI 표시 모델의 보정형 기본값. 같은 파일의 finalWriteDate·accountId·password 가 같은 패턴이라 serviceName 만 떼면 오히려 어긋나고, 그중 둘은 PR #1169 가 이미 손대는 중이다.

타임레터와 같게 다루지 말 것

ReceivedTimeLetterResponse.java:92 는 isAvailable ? timeLetter.getTitle() : null 로 진짜 조건부 null 이다(senderName:96·createdAt:98·isRead:99 도 같은 게이트). 전달 시점 전에는 제목조차 가린다는 설계이므로, 타임레터 쪽 nullable 은 근거가 있다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions