Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@ data class DailyQuestionCreateRequestDto(
@SerialName("content") val content: String,
@SerialName("isDraft") val isDraft: Boolean,
@SerialName("questionId") val questionId: Long,
@SerialName("imageUrl") val imageUrl: String? = null,
// 응답/요청 계약에 없는 필드라 타입은 nullable 로 두되 기본값은 두지 않는다.
// 매퍼가 항상 명시적으로 넘기므로 생략이 조용히 통과할 자리가 없다 (#789).
@SerialName("imageUrl") val imageUrl: String?,
)

/**
* `PATCH /daily-questions/{id}` 요청. 선택 필드라 nullable 은 유지하되 기본값은 두지 않는다 —
* 모든 생성 인자를 명시해야 "보내지 않음" 과 "명시적 null" 이 호출부 코드에서 구분된다 (#789).
*/
@Serializable
data class DailyQuestionUpdateRequestDto(
@SerialName("content") val content: String? = null,
@SerialName("isDraft") val isDraft: Boolean? = null,
@SerialName("date") val date: String? = null,
@SerialName("questionId") val questionId: Long? = null,
@SerialName("imageUrl") val imageUrl: String? = null,
@SerialName("content") val content: String?,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal] 요청 와이어 변화 — 키 생략 → "key":null 명시 송신.

= null 기본값 제거로, encodeDefaults=false 에서 생략되던 키가 explicitNulls(kotlinx 기본 true)로 항상 나간다. 유닛 실측: PATCH 본문이 {"content","isDraft","questionId"} → + "date":null, "imageUrl":null. 실서버 실측으로 현 서버는 null 을 무시함을 확인해(리뷰 본문 검증 2) 당장 깨지진 않지만:

  • DeliveryVerificationRequestDto(ReceiverAuthDto.kt, feat(afternote): 사망 증빙 서류를 둘 중 하나만 제출해도 열람 신청 가능하도록 완화 #380)가 정확히 반대 관용구 — "= null 기본값은 encodeDefaults=false 와 맞물려 키 자체가 생략된다(\"...\":null 을 명시 전송하지 않음)" — 를 KDoc 으로 선언하고 DeliveryVerificationContractTest 로 고정 중이다. 레포 안에 요청 직렬화 규약이 두 개가 됐다.
  • 위 KDoc 의 "모든 생성 인자를 명시해야 '보내지 않음'과 '명시적 null' 이 구분된다"는 성립하지 않는다: 도메인 DailyQuestionUpdatePayload / DailyQuestionCreatePayload / DiaryCreatePayload 가 해당 필드의 = null 기본값을 유지해 생략 자리가 한 층 위로 옮겨졌을 뿐이고(DailyQuestionWriteViewModel.kt:147 이 실제로 date 를 생략), 와이어에서는 "보내지 않음" 표현 자체가 사라졌다.

@SerialName("isDraft") val isDraft: Boolean?,
@SerialName("date") val date: String?,
@SerialName("questionId") val questionId: Long?,
@SerialName("imageUrl") val imageUrl: String?,
)

@Serializable
Expand All @@ -35,12 +41,17 @@ data class DailyQuestionListItemDto(
@SerialName("title") val title: String,
@SerialName("content") val content: String,
@SerialName("createdAt") val createdAt: String,
// Swagger `DailyQuestionListResponse` 응답 계약에 없는 필드 — 서버가 주기 시작하면 쓰이고,
// 아니면 계속 null. 계약에 없으므로 여기서만 기본값을 유지한다 (#789).
@SerialName("imageUrl") val imageUrl: String? = null,
// Swagger `DailyQuestionListResponse` 및 실서버 응답 모두 와이어 키는 `isDraft`.
// 과거 QA logcat 에서 `draft` 로 관측된 적이 있어 대체 키도 함께 허용한다.
//
// 기본값 `false` 는 두지 않는다 — 서버가 항상 보내는 키라, 빠졌을 때 "임시저장 아님" 으로
// 접히면 임시저장 글이 목록에 그대로 노출된다. 계약 누락은 파싱 실패로 드러나야 한다 (#789).
@SerialName("isDraft")
@JsonNames("draft")
val isDraft: Boolean = false,
val isDraft: Boolean,
)

@Serializable
Expand All @@ -54,12 +65,15 @@ data class TodayDailyQuestionDto(
@SerialName("day") val day: Int,
@SerialName("content") val content: String,
// Swagger `DailyQuestionTodayResponse` 및 실서버 응답 모두 `isAnswered`/`isDraft`.
// 종전 `answered`/`draft` 로 잡혀 있어 필수 필드 누락(MissingFieldException)으로
// getToday() 가 항상 실패했다 (#548). 과도기 대비로 구 키도 함께 받고 기본값을 둔다.
// 종전 DTO 가 `answered`/`draft` 로 잡혀 있어 필수 필드 누락(MissingFieldException)으로
// getToday() 가 항상 실패했다 (#548). 과도기 대비로 구 키는 계속 함께 받는다.
//
// 기본값은 두지 않는다 — 두 키 다 서버가 항상 보내므로, 기본값이 있으면 이번 같은 키
// 불일치가 다시 나도 "미답변·임시저장 아님" 인 정상 응답으로 조용히 통과한다 (#789).
@SerialName("isAnswered")
@JsonNames("answered")
val isAnswered: Boolean = false,
val isAnswered: Boolean,
@SerialName("isDraft")
@JsonNames("draft")
val isDraft: Boolean = false,
val isDraft: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ data class DiaryCreateRequestDto(
@SerialName("content") val content: String,
@SerialName("isDraft") val isDraft: Boolean,
@SerialName("todayMood") val todayMood: TodayMoodDto,
@SerialName("imageUrl") val imageUrl: String? = null,
@SerialName("receiverIds") val receiverIds: List<Long>? = null,
// 응답 계약에 없는 필드 — 타입만 nullable 로 두고 기본값은 두지 않는다 (#789).
@SerialName("imageUrl") val imageUrl: String?,
// 생성 API 에서 `null` 과 빈 목록은 모두 "수신자 없음" 으로 정규화되고, 작성 UI 는 항상
// 목록을 갖고 있다. 빈 목록을 그대로 보내면 되므로 nullable 로 낮추지 않는다 (#789).
@SerialName("receiverIds") val receiverIds: List<Long>,
)

@Serializable
Expand All @@ -36,23 +39,20 @@ data class DiaryUpdateRequestDto(
@SerialName("isDraft") val isDraft: Boolean,
@SerialName("todayMood") val todayMood: TodayMoodDto,
@SerialName("date") val date: String,
@SerialName("imageUrl") val imageUrl: String? = null,
@SerialName("imageUrl") val imageUrl: String?,
)

/**
* `/diary` 목록 항목 (Swagger `DiaryResponse` 실측, 2026-08-03).
* `/diary` 목록 항목 (Swagger `DiaryResponse`, 실서버 응답 실측 2026-08-23).
*
* 식별자를 뺀 나머지에 기본값을 두는 이유는 **실패의 폭**이다. 이 DTO 는 목록 원소라,
* 필드 하나가 비어 `MissingFieldException` 이 나면 그 항목이 아니라 **그 달 목록 전체**가
* 날아간다. 반면 값이 없을 때 잃는 것은 카드 한 장의 표시 요소뿐이다.
*
* `todayMood` 를 nullable 로 둔 것도 같은 맥락이다. non-null 이면 클라가 모르는 기분 값
* 하나에 목록이 통째로 사라진다 (`coerceInputValues` 는 기본값이 있어야 동작한다).
* 실제로 `emotion` 은 한글로 내려오는 중이다 (#591).
* 식별자를 뺀 나머지에도 기본값을 두지 않는다. 이 필드들은 서버가 항상 채워 보내는 값이라,
* 기본값이 있으면 응답 키 누락과 계약 변경이 파싱 실패가 아니라 **정상적인 빈 값**으로 바뀐다.
* 특히 `isDraft` 가 `false` 로 접히면 임시저장 일기가 목록에 노출되고, `date` 가 빈 문자열이
* 되면 캘린더에서 기록이 통째로 사라진다 (#789).
*
* (문서의 `required` 목록은 근거로 쓰지 않는다 — springdoc 은 `@Schema(requiredMode)` 가
* 없으면 non-null 프로퍼티도 required 에 넣지 않아, 비어 있다는 사실이 서버가 그 필드를
* 생략한다는 신호가 아니다. 응답 필드의 nullable 판단 기준 자체는 #676 소관.)
* 생략한다는 신호가 아니다. 판단은 실제 응답과 저장 컬럼 필수 여부를 기준으로 한다.)
*
* `id` 는 노션 명세("Diary 조회") 예시의 키다. Swagger 에는 없지만 두 문서가 갈려 있어
* 대체 키로 함께 받는다 — 실제 응답에 `id` 가 없어 충돌하지 않는다.
Expand All @@ -62,30 +62,36 @@ data class DiaryListItemDto(
@SerialName("diaryId")
@JsonNames("id")
val diaryId: Long,
@SerialName("title") val title: String = "",
@SerialName("content") val content: String = "",
@SerialName("title") val title: String,
@SerialName("content") val content: String,
/**
* 사용자가 고른 **일기의 날짜** (`format: date`, 예 `"2026-03-21"`).
*
* [createdAt] 과 **별개 필드**다 — 작성 화면의 날짜 선택 값이 여기 들어가고,
* `createdAt` 은 레코드가 만들어진 시각이다. 캘린더에 찍어야 하는 쪽은 이 값이다.
* 둘을 `@JsonNames` 로 한 프로퍼티에 묶으면 서버의 키 순서에 따라 값이 뒤바뀐다.
*/
@SerialName("date") val date: String? = null,
@SerialName("createdAt") val createdAt: String = "",
@SerialName("date") val date: String,
@SerialName("createdAt") val createdAt: String,
// Swagger `DiaryResponse` 에 없는 필드 — 서버가 주기 시작하면 쓰이고, 아니면 계속 null.
@SerialName("imageUrl") val imageUrl: String? = null,
@SerialName("todayMood") val todayMood: TodayMoodDto? = null,
// 저장 컬럼이 필수라 응답도 항상 채워진다. AI 가 매기는 `emotion` 과 달리 사용자가 직접
// 고른 값이고, 한글 값이 관측된 쪽도 `emotion` 이지 이 필드가 아니다 (#591, #789).
@SerialName("todayMood") val todayMood: TodayMoodDto,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal] 같은 응답 안에서 이 필드만 엄격 — 값 하나에 그 달 전체.

기본값이 없어 coerceInputValues 가 무력하고, 모르는 값 하나에 SerializationException 으로 그 달 목록 전체가 실패한다(유닛 실측: does not contain element with name 'SMILE' at $.data.diaries[0].todayMood). 같은 응답의 파생값인 weeklyDominantMood 에만 관대 직렬화기가 붙었다. 이슈 지시가 non-null 이고 assumptions.md(#897) 에 수용 리스크·되돌리는 방법이 기록된 것은 확인했다. 다만:

  • 실패 시 홈은 GetHomeSummaryUseCase 의 ?: 0 폴백으로 에러 표시 없이 "이번 달 0건" 이 된다 — PR 이 막겠다던 "정상적인 빈 값"이 홈에서 재현된다.
  • NullableTodayMoodSerializer KDoc("이모지 한 칸 대신 그 주/그 달 목록 전체를 날릴 일이 아니다")과 정면 모순이라, 다음 수정자가 어느 쪽이 정책인지 알 수 없다.
  • 구조 제안: 관대 처리를 TodayMoodDto 클래스 레벨 직렬화기로 올리면 필드별 어노테이션 없이 사용처 전부가 자동 커버되고, 생성자 기본값이 없는 한 키 누락은 여전히 MissingFieldException 이라 이 PR 의 목적과 상충하지 않는다.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal] 같은 응답 안에서 todayMood 만 엄격합니다 — 값 하나에 그 달 전체가 죽습니다.

이 필드는 기본값이 없어 coerceInputValues 가 무력하고, 모르는 값 하나에 SerializationException 으로 그 달 목록 전체가 실패합니다(유닛 실측: does not contain element with name 'SMILE' at $.data.diaries[0].todayMood). 같은 응답의 파생값인 weeklyDominantMood 에만 관대 직렬화기가 붙었습니다. 이슈 지시가 non-null 이고 assumptions.md(#897)에 수용 리스크·되돌리는 방법이 기록된 것은 확인했습니다. 다만 이 PR 안에서 정리할 가치가 있는 것:

  • 실패 시 홈은 GetHomeSummaryUseCase 의 ?: 0 폴백으로 에러 표시 없이 "이번 달 0건" 이 됩니다 — PR 이 막겠다던 "정상적인 빈 값"이 홈에서 재현됩니다.
  • NullableTodayMoodSerializer KDoc("이모지 한 칸 대신 그 주/그 달 목록 전체를 날릴 일이 아니다")과 정면 모순이라, 다음 수정자가 어느 쪽이 정책인지 알 수 없습니다.
  • 구조 제안: 관대 처리를 TodayMoodDto 클래스 레벨 직렬화기로 올리면 필드별 어노테이션 없이 사용처 전부가 자동 커버되고, 생성자 기본값이 없는 한 키 누락은 여전히 MissingFieldException 이라 이 PR 의 목적과 상충하지 않습니다.

@SerialName("isDraft")
@JsonNames("draft")
val isDraft: Boolean = false,
val isDraft: Boolean,
)

// `/diary` 응답의 `data` 는 객체 — `diaries` 외에 조회 대상 달의 비-임시 다이어리 수
// (`monthDiaryCount`)와 최근 7일 최빈 기분(`weeklyDominantMood`)이 함께 내려옴.
@Serializable
data class DiaryListDto(
@SerialName("diaries") val diaries: List<DiaryListItemDto> = emptyList(),
@SerialName("monthDiaryCount") val monthDiaryCount: Int = 0,
@SerialName("weeklyDominantMood") val weeklyDominantMood: TodayMoodDto? = null,
@SerialName("diaries") val diaries: List<DiaryListItemDto>,
@SerialName("monthDiaryCount") val monthDiaryCount: Int,
// 그 주에 기록이 없으면 서버가 `null` 을 **명시적으로** 보낸다. 값이 없다는 뜻이 실제로
// 있으므로 nullable 은 유지하되, 키 자체는 계약이라 기본값은 두지 않는다 (#789).
@SerialName("weeklyDominantMood")
@Serializable(with = NullableTodayMoodSerializer::class)
val weeklyDominantMood: TodayMoodDto?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
/** `GET /receiver-auth/daily-question` 응답 (`data`). */
@Serializable
data class ReceiverDailyQuestionListDto(
@SerialName("dailyQuestions") val dailyQuestions: List<ReceiverDailyQuestionItemDto> = emptyList(),
@SerialName("dailyQuestions") val dailyQuestions: List<ReceiverDailyQuestionItemDto>,
)

@Serializable
Expand All @@ -17,26 +17,32 @@ data class ReceiverDailyQuestionItemDto(
@SerialName("content") val content: String,
// "yyyy.MM.dd 요일" 형식.
@SerialName("createdAt") val createdAt: String,
// Swagger `DailyQuestionListResponse` 계약에 없는 필드 — 기본값을 유지한다 (#789).

@OnlyReadOneBook OnlyReadOneBook Aug 23, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal→정정] 이 주석이 인용하는 DailyQuestionListResponse 는 발신자 GET /daily-questions 의 스키마라 근거가 다른 엔드포인트에 걸려 있다 — 철회: Swagger 실측(#956 조사 중) 결과 GET /receiver-auth/daily-question 응답 항목이 발신자와 같은 DailyQuestionListResponse 스키마를 재사용함을 확인했다. 인용은 정당하다. 다만 "수신자 응답이 이 스키마를 재사용한다"는 한 마디가 주석에 있으면 오인용으로 읽히지 않겠다. (재사용은 isDraft 도 이 응답 계약에 있다는 뜻이라, 매퍼의 isDraft = false 하드코딩 건 #956 으로 이어졌다.)

@SerialName("imageUrl") val imageUrl: String? = null,
)

/** `GET /receiver-auth/diary` 응답 (`data`). */
@Serializable
data class ReceiverDiaryListDto(
@SerialName("diaries") val diaries: List<ReceiverDiaryItemDto> = emptyList(),
@SerialName("diaries") val diaries: List<ReceiverDiaryItemDto>,
)

/**
* 수신자용 일기 항목. 발신자 목록과 **같은 `DiaryResponse` 계약**을 쓰므로
* nullability 와 기본값도 [DiaryListItemDto] 와 동일하게 맞춘다 (#789).
*/
@Serializable
data class ReceiverDiaryItemDto(
@SerialName("diaryId") val diaryId: Long,
@SerialName("title") val title: String,
@SerialName("content") val content: String,
@SerialName("isDraft") val isDraft: Boolean = false,
@SerialName("isDraft") val isDraft: Boolean,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal] 수신자 DTO — "같은 계약" 동기화가 절반입니다.

ReceiverDiaryItemDto 는 "발신자 목록과 같은 DiaryResponse 계약이라 동일하게 맞춘다"고 선언하지만:

  • 쌍둥이 DiaryListItemDto 가 가진 @JsonNames("draft") / @JsonNames("id") 대체 키는 복제되지 않았습니다. 종전에는 = false 기본값이 (틀리게나마) 흡수했지만, 이제 서버가 과거 QA logcat 에서 실관측된 draft 키로 응답하면 발신자 목록은 정상 파싱되고 수신자 일기 화면만 전멸합니다 — 두 사본의 동작 격차를 이 PR 이 오히려 벌렸습니다.
  • todayMood · updatedAt 은 MindRecordMapper 가 읽지 않는 필드인데(도메인 MindRecordSummary 에 대응 항목 없음) 필수·닫힌 enum 으로 올라, 화면에 표시되지도 않는 값이 수신자 목록 전체를 죽일 수 있게 됐습니다. MindRecordReceiverRepositoryImpl.getAll() 이 단일 runCatching 이라 데일리질문 목록까지 연쇄로 사라집니다.
  • 참고로 /receiver-auth/diary 는 dev 에서 403(전달 조건 미충족)이라 실측 검증이 불가능한 경로입니다(리뷰 시점 authCode 로 재확인). 실측 못 하는 계약일수록 대체 키·관대성은 발신자 쪽과 같게 가져가는 편이 안전합니다.

// 응답 계약에 없는 필드 — 기본값을 유지한다.
@SerialName("imageUrl") val imageUrl: String? = null,
@SerialName("todayMood") val todayMood: TodayMoodDto? = null,
@SerialName("todayMood") val todayMood: TodayMoodDto,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal] "같은 계약" 동기화가 절반이다.

  • 쌍둥이 DiaryListItemDto 가 가진 @JsonNames("draft") / @JsonNames("id") 대체 키가 복제되지 않았다. 종전에는 = false 기본값이 (틀리게나마) 흡수했지만, 이제 서버가 과거 QA logcat 에서 실관측된 draft 키로 응답하면 발신자 목록은 정상 파싱되고 수신자 일기 화면만 전멸한다 — 두 사본의 동작 격차를 이 PR 이 오히려 벌렸다.
  • todayMood · updatedAt 은 MindRecordMapper 가 읽지 않는 필드인데(도메인 MindRecordSummary 에 대응 항목 없음) 필수·닫힌 enum 으로 올라, 화면에 표시되지도 않는 값이 수신자 목록 전체를 죽일 수 있게 됐다. MindRecordReceiverRepositoryImpl.getAll() 이 단일 runCatching 이라 데일리질문 목록까지 연쇄로 사라진다.
  • 참고로 /receiver-auth/diary 는 dev 에서 403(전달 조건 미충족)이라 실측 검증이 불가능한 경로다(리뷰 중 authCode 로 재확인). 실측 못 하는 계약일수록 대체 키·관대성은 발신자 쪽과 같게 가져가는 편이 안전하다.

// 작성일 (yyyy-MM-dd).
@SerialName("date") val date: String = "",
@SerialName("date") val date: String,
// "yyyy.MM.dd 요일" 형식.
@SerialName("createdAt") val createdAt: String = "",
@SerialName("updatedAt") val updatedAt: String = "",
@SerialName("createdAt") val createdAt: String,
@SerialName("updatedAt") val updatedAt: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.afternote.feature.mindrecord.data.dto

import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.nullable
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
* 값 자체가 조건부인 **nullable 기분 필드** 전용 직렬화기.
*
* `coerceInputValues` 는 생성자 기본값이 있어야 동작한다. #789 로 기본값을 걷어내면서
* 이 필드들도 함께 엄격해졌는데, 그 결과 "키 누락" 뿐 아니라 "클라가 모르는 **값**" 까지
* 파싱 실패가 됐다. 두 상황은 성격이 다르다.
*
* - 키 누락 → 계약이 바뀐 것. 실패해야 드러난다.
* - 모르는 값 → 서버가 기분 종류를 늘렸거나 표기가 바뀐 것. 그 주/그 달 목록 전체를
* 날릴 일이 아니라 이모지 한 칸만 비우면 된다 (#591 에서 감정 값이 한글로 관측된 전례).
*
* 그래서 키의 존재는 계약으로 두고(`= null` 기본값 없음), 값만 관대하게 접는다.
*/
object NullableTodayMoodSerializer : KSerializer<TodayMoodDto?> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("com.afternote.mindrecord.NullableTodayMood", PrimitiveKind.STRING).nullable

override fun deserialize(decoder: Decoder): TodayMoodDto? {
if (!decoder.decodeNotNullMark()) {
return decoder.decodeNull()
}
val raw = decoder.decodeString()
return TodayMoodDto.entries.firstOrNull { it.name == raw }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal] @SerialName 우회 — 와이어 표기 정본이 둘이 된다.

it.name == raw(이 줄) · value.name(40행)은 TodayMoodDto 가 상수마다 명시한 @SerialName 을 무시한다. 오늘은 두 표기가 우연히 일치하지만, 서버 표기 변경에 맞춰 @SerialName 만 고치는 순간 이 직렬화기가 붙은 두 필드만 예외 없이 전부 null(= "모르는 값" 정상 동작)이 되어 테스트로도 안 잡힌다. TodayMoodDto.serializer() 위임(또는 descriptor 기반 매칭)으로 정본을 하나로 유지하길 권한다.

부수 2건: 비문자열 입력(숫자·객체)은 decodeString 에서 응답 전체를 죽여 "값만 관대" 의도와 갈리고, 이 파일만 @file:OptIn(ExperimentalSerializationApi::class) 이 없어 opt-in 경고 3건이 새로 유입된다(같은 패키지 관례와 어긋남).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[normal] 이 직렬화기가 TodayMoodDto 의 @SerialName 을 우회합니다.

it.name == raw(33행) · value.name(40행)은 상수마다 명시된 @SerialName 을 무시합니다. 오늘은 두 표기가 우연히 일치하지만, 서버 표기 변경에 맞춰 @SerialName 만 고치는 순간 이 직렬화기가 붙은 두 필드만 예외 없이 전부 null(= "모르는 값" 정상 동작)이 되어 테스트로도 안 잡힙니다. TodayMoodDto.serializer() 위임(또는 descriptor 기반 매칭)으로 와이어 표기 정본을 하나로 유지하길 권합니다.

부수 2건: 비문자열 입력(숫자·객체)은 decodeString 에서 응답 전체를 죽여 "값만 관대" 의도와 갈리고, 이 파일만 @file:OptIn(ExperimentalSerializationApi::class) 이 없어 opt-in 경고 3건이 새로 유입됩니다(같은 패키지 관례와 어긋남).

}

override fun serialize(
encoder: Encoder,
value: TodayMoodDto?,
) {
if (value == null) encoder.encodeNull() else encoder.encodeString(value.name)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ package com.afternote.feature.mindrecord.data.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* `GET /mind-record` 응답 (`data`). 카운트·요약·목록 어느 것도 기본값을 두지 않는다 —
* 수치가 `0` 으로, 목록이 빈 목록으로 접히면 잘못된 주간 리포트가 정상 화면으로 보인다 (#789).
*/
@Serializable
data class WeeklyReportDto(
@SerialName("dailyQuestionAmount") val dailyQuestionAmount: Int = 0,
@SerialName("diaryAmount") val diaryAmount: Int = 0,
@SerialName("summaryText") val summaryText: String = "",
@SerialName("week") val week: List<WeeklyReportDayDto> = emptyList(),
@SerialName("dailyQuestionAmount") val dailyQuestionAmount: Int,
@SerialName("diaryAmount") val diaryAmount: Int,
@SerialName("summaryText") val summaryText: String,
@SerialName("week") val week: List<WeeklyReportDayDto>,
// 서버 JSON 키가 kebab-case (`daily-question`).
@SerialName("daily-question") val dailyQuestions: List<WeeklyReportDailyQuestionDto> = emptyList(),
@SerialName("emotions") val emotions: List<WeeklyReportEmotionDto> = emptyList(),
@SerialName("daily-question") val dailyQuestions: List<WeeklyReportDailyQuestionDto>,
@SerialName("emotions") val emotions: List<WeeklyReportEmotionDto>,
)

/**
Expand All @@ -25,28 +29,31 @@ data class WeeklyReportDto(
* 종전 DTO 는 존재하지 않는 `isDiary` 키를 읽어 항상 기본값 `false` 가 됐고, 그래서 모든
* 일기가 non-diary 로 접혀 캘린더 점과 기록일수에서 빠졌다.
*
* 세 필드는 명세가 required 로 선언한 값이라 기본값을 두지 않는다 — 빠지면 실패해야
* 계약 누락이 드러난다. 다만 [type] 은 `enum` 대신 `String` 으로 받는다. 서버가 종류를
* 하나 더 추가해도(현재 3종) 그 주 전체가 죽지 않고 매퍼에서 "일기 아님" 으로 접히면
* 되기 때문이다 — 값 집합은 닫혀 있지 않지만 키의 존재는 계약이다.
* 네 필드 모두 기본값을 두지 않는다 — 빠지면 실패해야 계약 누락이 드러난다. [emotion] 은
* 값 자체가 조건부라 nullable 은 유지하지만, 서버가 키는 명시적으로 보내므로 기본값은
* 없다 (#789). 다만 [type] 은 `enum` 대신 `String` 으로 받는다. 서버가 종류를 하나 더
* 추가해도(현재 3종) 그 주 전체가 죽지 않고 매퍼에서 "일기 아님" 으로 접히면 되기
* 때문이다 — 값 집합은 닫혀 있지 않지만 키의 존재는 계약이다.
*/
@Serializable
data class WeeklyReportDayDto(
@SerialName("diaryId") val diaryId: Long,
@SerialName("day") val day: Int,
@SerialName("type") val type: String,
@SerialName("emotion") val emotion: TodayMoodDto? = null,
@SerialName("emotion")
@Serializable(with = NullableTodayMoodSerializer::class)
val emotion: TodayMoodDto?,
)

@Serializable
data class WeeklyReportDailyQuestionDto(
@SerialName("title") val title: String = "",
@SerialName("content") val content: String = "",
@SerialName("date") val date: String = "",
@SerialName("title") val title: String,
@SerialName("content") val content: String,
@SerialName("date") val date: String,
)

@Serializable
data class WeeklyReportEmotionDto(
@SerialName("keyword") val keyword: String = "",
@SerialName("percentage") val percentage: Int = 0,
@SerialName("keyword") val keyword: String,
@SerialName("percentage") val percentage: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fun DiaryListItemDto.toDomain(): Diary =
content = content,
date = date,
createdAt = createdAt,
todayMood = todayMood?.toDomain(),
todayMood = todayMood.toDomain(),
imageUrl = imageUrl,
isDraft = isDraft,
)
Expand Down
Loading
Loading