Repository navigation
refactor(receiver): 수신자 상세 애프터노트 제목의 근거 없는 nullable 을 걷는다 — 도달 불가능한 «제목 없는 화면» #1177
Description
📜 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 이다. 검토된 결정이 아니라 손대지 않은 초기값이다.
할 일
ReceivedAfternoteDetailDto.title→String(기본값 없음). 키가 빠지면MissingFieldException으로 시끄럽게 실패한다 —NetworkModule.provideJson의coerceInputValues = true는 기본값이 있어야 동작하므로 이 필드를 조용히 구해주지 않는다.ReceivedAfternoteDetail.title→String(기본값 없음).- 매퍼의
title.orEmpty()2곳(ReceivedAfternoteDetailSuccessMapper.kt:34·44) 제거. - 딸려 나오는 테스트 조정 —
ReceivedAfternoteDetailMapperTest의title미지정 생성 5곳,LeaveMessageBlockContractTest.kt:104의title키 없는 JSON 픽스처. androidTest 3곳(AppAndReceiverCompletionAndroidTest2 ·ReceiverAdvancedAndroidTest1)은 이미title을 명시하고 있어 영향 없다. - 제목 키가 빠진 응답이 성공하지 않고 실패함을 계약 테스트로 고정한다.
완료 기준
ReceivedAfternoteDetailDto·ReceivedAfternoteDetail의title이 non-null 이고 기본값이 없다.- 매퍼에
title폴백이 남아 있지 않다. - 서버가
title키를 빼면 파싱이 성공하지 않는다(테스트로 고정). :app:compileDebugUnitTestKotlin포함 전 모듈 컴파일 green.
📌 Child Issue(Optional)
No response
📍 Note (Optional)
인접 이슈와의 경계
- 카테고리 실패의 폭을 항목 단위로 좁힌다 — 와이어 관용·도메인 엄격, 매퍼가 기각·보고 #1010 — 같은 DTO·모델·매퍼의
category/typenullable 을 다루며 «와이어 관용 · 도메인 엄격 · 매퍼가 항목 단위 기각» 방침을 세웠다. 그 관용 논거는 페이징 목록의 실패 폭(ReceiverAfternotePagingSource.load()가 항목 하나 때문에 페이지 전체를 날림)이고, 카테고리 실패의 폭을 항목 단위로 좁힌다 — 와이어 관용·도메인 엄격, 매퍼가 기각·보고 #1010 자신이 "결정할 것 2. 상세 경로의 기각 의미" 를 미해결로 남겼다. 상세는 응답 하나가 화면 하나라 «항목만 떨어뜨린다» 가 성립하지 않고, 제목 없는 상세는 어차피 못 그린다 — 그래서 이 이슈는 상세 한정으로 와이어까지 엄격을 택한다. 목록(ReceivedAfternoteDto)은 손대지 않는다. - refactor(afternote): 응답 DTO 보정형 기본값 정리 — 누락 계약을 빈 값으로 은폐하지 않는다 (#789·#790 애프터노트 판) #957 — 같은
ReceiverAfternoteDto.kt의 보정형 기본값 4건(emptyList()·0)을 다룬다.title: String? = null은 보정형 기본값이 아니라 불필요한 nullable 이라 그 목록에 없다. 같은 파일이므로 착수 순서만 조율한다. - refactor(dto): 응답 DTO 를 BE 실코드에 동기화 — 유령 필드 9건·누락 필드 21건 #676 — 필드 구성(유령·누락) 동기화. nullability 축은 다루지 않으며, 이 DTO 에 대해서는
credentials유령 여부만 «판단 필요» 로 남아 있다. - PR fix(receiver): 남기지 않은 계정 정보에 마스킹을 그리지 않는다 (closes 619) [base: develop] #1169(fix/619) — 같은 매퍼의
credentials쪽orEmpty()를 이미 걷어냈고serviceName = title.orEmpty()바로 아랫줄을 고친다. 텍스트 충돌을 감안한다.
범위 밖
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 은 근거가 있다.
Activity
Merge branch 'develop' into fix/619