You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(afternote): 수정 시 변경하지 않은 필드를 부분 PATCH에서 생략한다 #1617
Afternote-BE#277은 배포/머지 선행이다. 현재 서버는 playlist 안 songs 생략을 곡 없음으로 오인한다. FE 코드는 합의된 생략=유지 계약으로 준비하지만, 이 검증 수정 전에는 발행 노트의 미디어 단독 수정이 거절되므로 blocked_by를 유지한다.
기존 곡을 항상 다시 보내는 우회는 이 이슈의 덮어쓰기를 되살리므로 사용하지 않는다.
이 변경은 사용자가 건드리지 않은 필드의 덮어쓰기를 막는다. 같은 필드를 두 기기에서 동시에 바꾼 경우의 충돌 판정은 버전 토큰 등 별도의 서버 계약이 있어야 하며 현재 API에서 보장하지 않는다.
AfternoteUpdateRequest.java — 수정 전용 DTO 가 생성 DTO 에서 분리돼 있고, title·actions·leaveMessage·credentials·receivers·playlist·isDraft 가 전부 nullable 입니다. category 는 「생략 권장(변경 불가)」.
@Schema(description = "애프터노트 수정 요청. 수정하지 않을 필드는 생략 가능. category는 생략 권장(변경 불가).")
근거 3 — 「생략 = 유지」가 문서가 아니라 구현이다
AfternoteService.updateAfternote 가 필드마다 null 검사로 기존 값을 보존합니다.
playlist 안의 memorialPhotoUrl·memorialVideo·memorialAudioUrl 은 필드 생략 시 유지 / JSON null 이면 삭제(DB 참조 제거 + S3 객체 삭제) / 값이 있으면 교체 입니다. PlaylistRequestDeserializer 가 「키 없음」과 「명시적 null」을 갈라 봅니다. #1596 이 FE 에서 세운 명시적 null 표현이 이 축에 정확히 대응합니다.
그래서 어느 갈래로 가는가
본문 해야 할 일의 세 번째 항목 — 「대안으로 dirty 필드만 보내는 부분 PATCH」 — 가 지금 그대로 구현 가능한 갈래입니다. 버전 토큰·If-Match 협의를 기다릴 필요가 없습니다.
완료 조건의 두 갈래 중 뒤쪽을 만족합니다. 「애초에 그 필드가 요청에 실리지 않는다」쪽입니다.
재현 경로로 든 사고 — 「곡을 건드린 적 없는데 곡이 지워진다」 — 는 만지지 않은 필드가 요청에서 빠지므로 구조적으로 사라집니다.
다만 사용자가 실제로 만진 필드의 동시 편집 충돌은 여전히 last-write-wins 로 남습니다. 부분 PATCH 는 그 축을 닫지 않습니다.
BE main 에도 낙관적 동시성 제어는 아직 없습니다 — @Version·If-Match·ETag·updatedAt 어느 것도 애프터노트 도메인에 없습니다(실측 0건). 즉 1·2번 항목은 그대로 살아 있습니다.
그래서 부분 PATCH PR 은 Closes 가 아니라 Refs #1617 로 답니다. 이 이슈는 1·2번 항목이 남아 있으므로 열어 둡니다 — 이 축의 추적처는 계속 이 이슈입니다.
⚠️ 다만 머지 순서 제약이 하나 있다
이 계약은 main 에만 있고 origin/release 에는 없습니다.AfternoteUpdateRequest.java 파일 자체가 release 에 존재하지 않습니다.
$ git merge-base --is-ancestor bbff47c8 origin/release && echo YES || echo NO
NO
$ git cat-file -e origin/release:src/main/java/com/afternote/domain/afternote/dto/AfternoteUpdateRequest.java
fatal: path ... exists on disk, but not in 'origin/release'
release 시점 BE 는 수정 요청을 생성 DTO 로 받습니다. 키를 생략하면 「유지」가 아니라 @NotNull 검증 400 이거나 null 로 덮어쓰기가 됩니다. 즉 FE 부분 PATCH 가 실서버보다 먼저 나가면 수정 기능이 통째로 깨집니다.
→ FE PR 은 Draft 로 열고 본문에 머지 게이트를 명시합니다. BE 가 이 계약을 release 에 올린 뒤에 머지합니다.
changed the title [-]fix(afternote): 수정 PATCH 가 stale 스냅샷을 통째로 보내 남의 변경을 조용히 덮는다 — 낙관적 동시성 제어 부재[/-][+]fix(afternote): 수정 시 변경하지 않은 필드를 부분 PATCH에서 생략한다[/+]on Sep 8, 2026
이 BE 이슈가 열려 있는 동안 merge-order-guard 가 #1617 을 blocked 로 판정해 PR #1626 이하 스택 8건의 머지를 막는다. 덧붙여 배포 여부를 실측했다. BE 의 Deploy 워크플로는 push 트리거가 main 이고 d34353e9 는 main 에 있으며 그 실행이 success 다. release 브랜치는 배포 경로가 아니므로 이 이슈 본문의 배포 후 검증 게이트는 이미 충족된 상태다. 남은 것은 dev 서버 실측뿐이다.
물은 내용은 묵은 본문을 옮긴 것이 아니라 Afternote-BE main 실코드로 대조해 다시 벼린 것이다. 코드로 이미 답이 나온 축은 질문에서 빼고 사실로 적었다.
기준은 현재 https://afternote.kro.kr/v3/api-docs의 AfternoteCreateRequest·AfternoteUpdateRequest·PlaylistRequest입니다. 기존 QA 계정에서 수신자 없이 새 synthetic 발행 PLAYLIST를 만들고, 곡 1개가 있는 상태를 먼저 조회했습니다.
실행 시각은 2026-09-20 22:10 KST이며, 이번에 생성한 노트 ID 50만 수정·삭제했습니다. 기존 사용자 노트·타인 수신자는 변경하지 않았고 공유·알림 전송 API를 호출하지 않았습니다. 업로드나 storage key를 사용하지 않았으므로 이 검증은 곡 생략 처리와 발행 노트의 빈 곡 목록 거절 범위입니다. 실제 S3 객체 삭제를 검증한 것은 아닙니다.
BE #279 및 성공한 배포 실행의 구현·배포 근거에 더해, #1617의 남아 있던 실서버 PATCH 동작도 확인됐습니다.
Activity
github-actions commentedon Aug 30, 2026
bug 이슈는 사이드바 Fields > Priority 로 심각도를 지정해야 합니다. 판정 기준:
값이 비어 있어도 이슈를 닫지는 않습니다. 이 안내는 이슈당 한 번만 답니다.
OnlyReadOneBook commentedon Aug 30, 2026
정정 — 「BE 선행이므로 결론 전 FE 구현 금지」 게이트는 이미 풀렸다
본문 해야 할 일의 첫 항목에 「BE 선행이므로 결론 전에는 FE 구현 금지」 라고 적어 뒀지만, BE 부분 PATCH 계약은 이미
main에 머지돼 있습니다. 이 이슈(0830)보다 일주일 먼저 들어갔습니다. 로컬 BE 체크아웃에서 실측한 근거를 남깁니다.근거 1 — 계약을 세운 커밋이 이슈보다 앞선다
bbff47c8—refactor: 애프터노트 PATCH DTO 분리 및 draft/발행 검증 계약 정리 (#200, #201), 2026-08-23 12:15 KST.origin/main의 조상입니다.근거 2 — DTO 가 전 필드에 「생략 시 기존 값 유지」를 명시한다
AfternoteUpdateRequest.java— 수정 전용 DTO 가 생성 DTO 에서 분리돼 있고,title·actions·leaveMessage·credentials·receivers·playlist·isDraft가 전부 nullable 입니다.category는 「생략 권장(변경 불가)」.근거 3 — 「생략 = 유지」가 문서가 아니라 구현이다
AfternoteService.updateAfternote가 필드마다 null 검사로 기존 값을 보존합니다.null(생략) = 유지 / non-null = 치환 이므로, 빈 배열은 「전부 삭제」로 그대로 통합니다. #1596·#1599 가 세운 「빈 값 = 삭제」 계약과 정면으로 맞물립니다 — 두 계약이 충돌하지 않고 서로를 완성합니다.
근거 4 — 컨트롤러 테스트가 부분 전송 200 을 단언한다
AfternoteControllerTest#updateAfternote_OmitCategoryAndTitle_Success가{"actions":["a1"]}한 키만 보내고 200 을 단언합니다.category·title생략이 계약으로 못박혀 있습니다.근거 5 — 미디어는 tri-state 다
playlist안의memorialPhotoUrl·memorialVideo·memorialAudioUrl은 필드 생략 시 유지 / JSON null 이면 삭제(DB 참조 제거 + S3 객체 삭제) / 값이 있으면 교체 입니다.PlaylistRequestDeserializer가 「키 없음」과 「명시적 null」을 갈라 봅니다. #1596 이 FE 에서 세운 명시적 null 표현이 이 축에 정확히 대응합니다.그래서 어느 갈래로 가는가
본문 해야 할 일의 세 번째 항목 — 「대안으로 dirty 필드만 보내는 부분 PATCH」 — 가 지금 그대로 구현 가능한 갈래입니다. 버전 토큰·
If-Match협의를 기다릴 필요가 없습니다.BE
main에도 낙관적 동시성 제어는 아직 없습니다 —@Version·If-Match·ETag·updatedAt어느 것도 애프터노트 도메인에 없습니다(실측 0건). 즉 1·2번 항목은 그대로 살아 있습니다.그래서 부분 PATCH PR 은
Closes가 아니라Refs #1617로 답니다. 이 이슈는 1·2번 항목이 남아 있으므로 열어 둡니다 — 이 축의 추적처는 계속 이 이슈입니다.이 계약은
main에만 있고origin/release에는 없습니다.AfternoteUpdateRequest.java파일 자체가 release 에 존재하지 않습니다.release 시점 BE 는 수정 요청을 생성 DTO 로 받습니다. 키를 생략하면 「유지」가 아니라
@NotNull검증 400 이거나 null 로 덮어쓰기가 됩니다. 즉 FE 부분 PATCH 가 실서버보다 먼저 나가면 수정 기능이 통째로 깨집니다.→ FE PR 은 Draft 로 열고 본문에 머지 게이트를 명시합니다. BE 가 이 계약을 release 에 올린 뒤에 머지합니다.
github-actions commentedon Aug 30, 2026
이 이슈를 맡은 PR 은 #1626 다 (base
fix/1599-empty-songs-explicit).base 가 기본 브랜치(
develop)가 아니라서 GitHub 은 이 이슈의 Development 칸에아무것도 표시하지 않는다. 본문에
Closes #N이 있어도 마찬가지다 — 그 링크는 기본 브랜치를target 하는 PR 에서만 만들어진다.
PR 이 닫히거나 머지되면 이 이슈의
pr-open라벨은 다음 리컨사일에서 떨어진다.docs(afternote): baseline 주석의 상호 참조를 심볼 이름으로 바로잡는다 (#1617)
fix(afternote): 부분 PATCH 판정을 서버가 반영하는 슬롯 단위로 내린다 (#1617)
docs(afternote): 생성 전용이 된 MemorialWritePayload 의 KDoc 을 바로잡는다 (#1617)
fix(afternote): 수정 전용 플레이리스트 DTO 이름을 Request 규약에 맞춘다 (#1617)
OnlyReadOneBook commentedon Sep 2, 2026
PR #1626 을
Closes #1617로 올립니다. 완료 조건의 뒤 갈래(«안 만진 필드는 애초에 요청에 실리지 않는다»)를 채우고, 해야 할 일 1·2번은 대안 3번 채택으로 이 이슈의 완료 조건에 불필요해졌습니다.PR 이 이 이슈에 얹어 두던 두 축은 별건으로 뗐습니다.
blocked_by#1599 는 스택(#1602 위)이 처리하므로 그대로 둡니다.fix(afternote): 수정 PATCH 가 안 건드린 필드를 싣지 않게 한다 (#1617)
6 remaining items
merge: 부분 PATCH를 최신 Nav3와 미디어 삭제 계약에 통합 (#1617)
[-]fix(afternote): 수정 PATCH 가 stale 스냅샷을 통째로 보내 남의 변경을 조용히 덮는다 — 낙관적 동시성 제어 부재[/-][+]fix(afternote): 수정 시 변경하지 않은 필드를 부분 PATCH에서 생략한다[/+]OnlyReadOneBook commentedon Sep 13, 2026
Afternote-BE#277 에 코멘트로 아직 나간 적 없던 질문을 보냈다(2026-09-13).
이 BE 이슈가 열려 있는 동안 merge-order-guard 가 #1617 을 blocked 로 판정해 PR #1626 이하 스택 8건의 머지를 막는다. 덧붙여 배포 여부를 실측했다. BE 의 Deploy 워크플로는 push 트리거가 main 이고 d34353e9 는 main 에 있으며 그 실행이 success 다. release 브랜치는 배포 경로가 아니므로 이 이슈 본문의 배포 후 검증 게이트는 이미 충족된 상태다. 남은 것은 dev 서버 실측뿐이다.
물은 내용은 묵은 본문을 옮긴 것이 아니라 Afternote-BE main 실코드로 대조해 다시 벼린 것이다. 코드로 이미 답이 나온 축은 질문에서 빼고 사실로 적었다.
test: 복원 프리필 회귀 검증의 현재 스냅샷 키 사용 (#1617)
OnlyReadOneBook commentedon Sep 20, 2026
2026-09-20 배포 서버에서 미디어 단독 PATCH 계약 검증을 완료했습니다.
기준은 현재
https://afternote.kro.kr/v3/api-docs의AfternoteCreateRequest·AfternoteUpdateRequest·PlaylistRequest입니다. 기존 QA 계정에서 수신자 없이 새 synthetic 발행 PLAYLIST를 만들고, 곡 1개가 있는 상태를 먼저 조회했습니다.isDraft=false, 곡 1개, 수신자 0명{"playlist":{"memorialPhotoUrl":null}}PATCH,songs생략{"playlist":{"songs":[]}}PATCHcode=1610code=1600실행 시각은 2026-09-20 22:10 KST이며, 이번에 생성한 노트 ID 50만 수정·삭제했습니다. 기존 사용자 노트·타인 수신자는 변경하지 않았고 공유·알림 전송 API를 호출하지 않았습니다. 업로드나 storage key를 사용하지 않았으므로 이 검증은 곡 생략 처리와 발행 노트의 빈 곡 목록 거절 범위입니다. 실제 S3 객체 삭제를 검증한 것은 아닙니다.
BE #279 및 성공한 배포 실행의 구현·배포 근거에 더해, #1617의 남아 있던 실서버 PATCH 동작도 확인됐습니다.
refactor(afternote): 수신자 참조 입력 모델 이름 정리 (#1617)
fix(afternote): 기준 스냅샷 오류 문구 명확화 (#1617)
test(afternote): 저장 유스케이스 테스트를 바뀐 수정 입력 모델 이름으로 맞춘다 (#1694)