Skip to content

[fix] 애프터노트 PATCH 발행 검증이 playlist 안 songs 생략을 «곡 없음» 으로 읽는다 — 발행된 추억 노트의 미디어만 수정하면 1610 #277

Closed

Description

@OnlyReadOneBook

문제

PATCH /api/v1/afternotes/{afternoteId} 의 발행 필수값 검증 AfternoteValidator.validatePlaylistPublish 가 요청에 playlist 키가 있으면 songs 가 null 이거나 비었을 때 곧장 PLAYLIST_SONGS_REQUIRED(400, 1610) 를 던진다. 기존 엔티티의 곡(afternote.getPlaylist().getItems())은 playlist 키가 없을 때만 본다.

private void validatePlaylistPublish(AfternoteCreateRequest request, Afternote afternote) {
    if (request.getPlaylist() != null) {
        if (request.getPlaylist().getSongs() == null || request.getPlaylist().getSongs().isEmpty()) {
            throw new CustomException(ErrorCode.PLAYLIST_SONGS_REQUIRED);   // songs 생략도 여기로 온다
        }
        return;
    }
    if (afternote.getPlaylist() == null || items == null || items.isEmpty()) {
        throw new CustomException(ErrorCode.PLAYLIST_REQUIRED);            // 기존 곡은 playlist 키가 없을 때만 본다
    }
}

그런데 같은 요청의 병합은 songs == null 을 «유지» 로 다룬다 — PlaylistRelationStrategy.update 는 getSongs() != null 일 때만 items 를 갈아 끼운다. 저장 로직은 «생략 = 유지» 를 지키는데 검증기만 «생략 = 없음» 으로 읽어, 실제로는 곡이 그대로 남을 요청을 거절한다.

DTO 문서와도 어긋난다. AfternoteUpdateRequest.playlist 설명은 «playlist 객체 생략 시 플레이리스트 전체 유지. 정식 등록 상태면 요청·기존 합쳐 필수» 인데, 합치는 건 playlist 키가 없는 경우뿐이다. 같은 파일의 credentials 발행 검증 validateCredentialsPublish 는 요청 값 또는 기존 secureContents 로 판정해 약속대로 합친다 — playlist 만 빠졌다.

재현 (발행된 PLAYLIST 노트, 곡 1개 이상 저장돼 있음)

PATCH /api/v1/afternotes/{afternoteId}
Content-Type: application/json

{"playlist":{"memorialPhotoUrl":null}}
  • 기대: 200. 영정 사진만 삭제되고 곡은 그대로.
  • 실제(코드 경로): 400 1610 플레이리스트에는 최소 1곡 이상이 필요합니다.

{"playlist":{"memorialVideo":null}}, {"playlist":{"memorialAudioUrl":null}}, 미디어 교체("memorialPhotoUrl":"afternotes/…") 도 같다 — playlist 안에 songs 를 함께 싣지 않는 모든 수정이 발행 노트에서 막힌다. 요청에 isDraft 가 없으면 저장값(false)으로 발행 상태가 정해지므로 클라이언트 쪽 우회도 없다. draft 노트는 검증기가 돌지 않아 무사하다.

AfternoteValidatorTest 의 수정·발행 테스트 3건은 기존 노트가 전부 draft(draftPlaylist()) 라서 «발행 노트 + 기존 곡 있음 + 요청에 songs 생략» 조합이 없다.

판정 리비전: origin/main 6364a6c (0831 deploy 성공 SHA, 실서버 동일). 코드 경로로 판정했고 실서버 호출 실측은 하지 않았다.

클라이언트 영향

FE 는 지금까지 수정 시 playlist 전체(songs 포함, 기본값 emptyList())를 통째로 보내서 이 검증을 우연히 통과했다. Afternote/Afternote-FE#1626 이 «안 건드린 필드는 키째 생략» 하는 부분 PATCH 로 바꾸면(Afternote/Afternote-FE#1617 lost update 대응) 사진·영상만 고친 요청이 아래 모양으로 나가고, 발행된 추억 노트의 미디어만 수정하는 경로가 전부 1610 이 된다.

{"category":"PLAYLIST","playlist":{"memorialPhotoUrl":null}}

FE 가 songs 를 늘 함께 싣는 우회는 FE#1617 이 없애려는 곡 축의 덮어쓰기(다른 기기가 그 사이 추가한 곡이 stale 스냅샷으로 지워짐)를 되살리므로, 서버 쪽에서 닫는 것이 맞다. 요청 모양은 PR 의 AfternoteUpdatePartialPatchWireTest 가 JSON 문자열로 고정해 두었다.

제안

validatePlaylistPublish 에서 요청 songs 가 null(생략) 이면 기존 afternote.getPlaylist().getItems() 로 대체해 최종 상태를 판정한다. 빈 배열 [] 은 «전부 삭제» 의사이므로 지금처럼 1610 을 유지한다.

private void validatePlaylistPublish(AfternoteCreateRequest request, Afternote afternote) {
    boolean hasExisting = afternote.getPlaylist() != null
            && afternote.getPlaylist().getItems() != null
            && !afternote.getPlaylist().getItems().isEmpty();

    if (request.getPlaylist() == null) {
        if (!hasExisting) throw new CustomException(ErrorCode.PLAYLIST_REQUIRED);
        return;
    }
    List<AfternoteCreateRequest.SongRequest> songs = request.getPlaylist().getSongs();
    if (songs == null) {                       // 생략 = 유지 → 기존 곡으로 판정
        if (!hasExisting) throw new CustomException(ErrorCode.PLAYLIST_SONGS_REQUIRED);
        return;
    }
    if (songs.isEmpty()) {                     // [] = 전부 삭제 → 발행 노트에선 거절
        throw new CustomException(ErrorCode.PLAYLIST_SONGS_REQUIRED);
    }
}

완료 기준

  • 발행된 PLAYLIST 노트에 {"playlist":{"memorialPhotoUrl":null}} 처럼 songs 없는 playlist 패치를 보내면 200 이고 기존 곡이 유지된다.
  • {"playlist":{"songs":[]}} 는 발행 노트에서 지금처럼 1610 이다.
  • draft→발행 전환(isDraft:false)에서 요청·기존 어느 쪽에도 곡이 없으면 지금처럼 1609/1610 이다.
  • AfternoteValidatorTest 에 «발행 노트 + 기존 곡 있음 + songs 생략 → 통과» 와 «발행 노트 + songs [] → 1610» 이 추가된다.
  • AfternoteUpdateRequest.playlist 설명의 «요청·기존 합쳐 필수» 가 실제 동작과 일치한다.

관련

Activity

  1. OnlyReadOneBook commented on Sep 13, 2026

    @OnlyReadOneBook
    ContributorAuthor

    @Hwanggyuun 님, 확인 두 가지만 부탁드립니다.

    1. Afternote-BE#277 을 지금 닫아도 될까요? (닫아도 된다 / 남은 작업이 있다)
    2. PR fix: 앱 패스키 origin 허용 · 발행 추억 노트 songs 생략을 유지로 맞춤 #279 로 나간 동작이 이게 맞나요? 발행 PLAYLIST PATCH 에서 songs 키 생략은 기존 곡 유지, "songs": [] 만 1610. (맞다 / 다르다)

    배포 시점은 따로 여쭙지 않습니다. 이미 확인했습니다. PR #279 가 2026-09-09 14:36:54Z 에 main 에 머지됐고(머지 커밋 d34353e979702e3065a949ab584c3415d9c9b86c), 그 커밋의 Deploy 워크플로 실행이 success 로 끝났습니다. main 의 AfternoteValidator.validatePlaylistPublish 도 songs 가 null 이고 기존 곡이 있으면 통과, songs.isEmpty() 면 PLAYLIST_SONGS_REQUIRED 로 갈라져 있습니다. 이슈만 OPEN 으로 남은 건 PR 본문에 Closes 가 걸리지 않아서로 보입니다. 코멘트도 0건입니다.

    맥락으로만 덧붙입니다. 이 이슈가 열려 있는 동안 FE 의 merge-order-guard 가 #1617 을 열린 #277 에 blocked 로 판정해 스택 8건(#1626, #1839, #1837, #1838, #1971, #1972, #2021, #1768)의 머지를 막습니다. 다만 같은 가드가 FE 자체 이슈 #1762 도 함께 걸고 있어서, #277 이 닫혀도 FE 쪽 선행이 하나 남습니다. 재촉이 아니라 저희가 풀 게 하나 줄어든다는 뜻입니다.

  2. OnlyReadOneBook commented on Sep 13, 2026

    @OnlyReadOneBook
    ContributorAuthor

    PR #279 로 해소된 것을 확인해 닫습니다. 근거를 어느 층까지 확인했는지 함께 적습니다.

    완료 기준 대조 (기준 리비전 main 3593aa3)

    1. 발행 노트에 songs 없는 playlist 패치 → 200, 곡 유지. AfternoteValidator.validatePlaylistPublish 가 songs == null 이면 hasExistingPlaylistSongs(afternote) 로 갈라 통과시킵니다. 이 이슈 본문이 제안한 코드와 분기·에러코드가 같습니다.
    2. songs 가 빈 배열이면 1610. songs.isEmpty() → PLAYLIST_SONGS_REQUIRED.
    3. draft 에서 발행으로 전환할 때 양쪽 다 곡이 없으면 1609 또는 1610. playlist 키 없음은 PLAYLIST_REQUIRED(1609), playlist 키가 있고 songs 생략이면 PLAYLIST_SONGS_REQUIRED(1610). ErrorCode.java:145-146 에서 번호를 확인했습니다.
    4. 테스트 2건. AfternoteValidatorTest 에 update_PublishedPlaylist_OmitSongs_KeepsExisting 과 update_PublishedPlaylist_EmptySongs_Fails 가 있습니다. NPE 가드 2건도 함께 들어가 있습니다.
    5. DTO 문서 일치. AfternoteUpdateRequest.playlist 설명에 songs 생략은 기존 곡 유지, 빈 배열은 전부 삭제라는 문장이 들어갔고 컨트롤러 @Operation 과 PlaylistRequest.songs 의 @ArraySchema 에도 같은 내용이 반영됐습니다. 요청드린 한 곳보다 넓게 고쳐 주셨습니다.

    저장 쪽과도 맞물립니다. PlaylistRelationStrategy.update 가 songs != null 일 때만 items 를 갈아 끼우므로, 검증기의 통과 조건이 저장의 유지 조건과 같은 축이 됐습니다. 이 이슈가 지적한 불일치가 사라졌습니다.

    배포도 확인했습니다. deploy.yml 트리거는 push: main 과 workflow_dispatch 뿐이고 워크플로 6개를 전수로 봐도 release 를 배포하는 경로는 없습니다. 머지 커밋 d34353e979 의 Deploy 실행은 build·deploy·smoke·contract 네 job 전부 success 였고, 이후 main 에 올라간 3593aa3 의 배포도 success 이며 그 시점 코드에도 고친 분기가 그대로 있습니다.

    확인하지 못한 것

    근거는 main 실코드와 단위 테스트 두 층입니다. 실서버에 PATCH 를 쏘아 본 기록은 아직 없습니다.

    • PR fix: 앱 패스키 origin 허용 · 발행 추억 노트 songs 생략을 유지로 맞춤 #279 본문 Test plan 체크박스 4개가 미체크이고 그중 둘이 위 완료 기준 1·2 그대로입니다.
    • 파이프라인도 이 축을 건드리지 않습니다. deploy.yml 의 빌드는 -x test 로 테스트를 빼고 scripts/contract-test.sh 는 --include-method GET 으로만 돕니다. Deploy success 는 해당 SHA 로 빌드한 이미지가 떠서 health 가 UP 이라는 것까지만 증명합니다.
    • 실제 왕복 실측은 Afternote-FE#1626 의 검증에서 처음 이뤄집니다. 거기서 어긋나는 게 나오면 이 이슈를 다시 열고 알려 드리겠습니다.

    PR #279 에 Closes 가 걸리지 않아 자동으로 닫히지 않은 것으로 보여 대신 닫습니다. 판단이 다르시면 다시 열어 주세요.

    참고로 Afternote-FE 의 merge-order-guard 가 이 이슈를 선행으로 보고 PR #1626 이하 스택 8건을 막고 있었습니다. 이제 그쪽 선행은 FE 자체 이슈 하나만 남습니다.

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions