diff --git a/backend/src/main/java/com/stackup/stackup/coverletter/application/CoverLetterService.java b/backend/src/main/java/com/stackup/stackup/coverletter/application/CoverLetterService.java index 8fb2d2ff..2046fb05 100644 --- a/backend/src/main/java/com/stackup/stackup/coverletter/application/CoverLetterService.java +++ b/backend/src/main/java/com/stackup/stackup/coverletter/application/CoverLetterService.java @@ -62,7 +62,11 @@ public List list(Long userId) { public void delete(Long userId, Long coverLetterId) { CoverLetter coverLetter = loadOwned(userId, coverLetterId); coverLetter.markDeleted(); - // 분석 결과 cascade — document 도메인 listener 가 받아 AnalyzedDocument soft delete (도메인 cycle 회피). + // 문항 원문 즉시 파기 — 자소서 본문은 S3 가 아니라 이 행 안에 살아서, cascade 파기 + // (분석 마크다운·임베딩)만으로는 "지웠는데 남아 있는" 상태가 된다. + coverLetter.purgeItems(); + // 분석 결과 cascade — document 도메인 listener 가 받아 AnalyzedDocument soft delete + // + 분석 내용물 파기 (도메인 cycle 회피). events.publishEvent(new CoverLetterDeletedEvent(userId, coverLetterId)); } diff --git a/backend/src/main/java/com/stackup/stackup/coverletter/domain/CoverLetter.java b/backend/src/main/java/com/stackup/stackup/coverletter/domain/CoverLetter.java index bccadaab..5f9996b0 100644 --- a/backend/src/main/java/com/stackup/stackup/coverletter/domain/CoverLetter.java +++ b/backend/src/main/java/com/stackup/stackup/coverletter/domain/CoverLetter.java @@ -76,6 +76,20 @@ public void markFailed() { this.status = CoverLetterStatus.FAILED; } + /** + * 문항 원문을 파기한다(빈 배열로). + * + *

자소서 본문은 지원동기·성장과정 같은 지극히 개인적인 서술이고, 다른 자료와 달리 + * S3 가 아니라 이 행 안에 산다. 삭제 시 분석 마크다운·임베딩은 cascade 가 + * 파기하지만(#219) 원문은 여기서 비워야 한다 — 삭제 후에는 아무도 읽지 않는다 + * (분석은 생성 시점에 본문을 메시지에 인라인한다). + * + *

컬럼이 NOT NULL DEFAULT '[]' 라 제약 변경 없이 빈 배열로 비운다. + */ + public void purgeItems() { + this.items = "[]"; + } + public void markDeleted() { this.deleted = true; } diff --git a/backend/src/main/resources/db/migration/V34__purge_items_of_deleted_cover_letters.sql b/backend/src/main/resources/db/migration/V34__purge_items_of_deleted_cover_letters.sql new file mode 100644 index 00000000..00108516 --- /dev/null +++ b/backend/src/main/resources/db/migration/V34__purge_items_of_deleted_cover_letters.sql @@ -0,0 +1,8 @@ +-- 자소서 원문(items)은 다른 자료와 달리 S3 가 아니라 행 안에 산다. 삭제 시 분석 +-- 마크다운·임베딩은 파기되지만(#219 cascade) 원문은 남았다. 이 릴리스부터 삭제 시 +-- 함께 비우는데, 그 전에 지운 자소서는 삭제 이벤트가 다시 나지 않으므로 백필한다 +-- (V31 탈퇴자 토큰 · #223 과거 삭제분 S3 객체와 같은 종류 — 새 규칙의 소급 적용). +UPDATE cover_letters +SET items = '[]'::jsonb +WHERE is_deleted = TRUE + AND items <> '[]'::jsonb; diff --git a/backend/src/test/java/com/stackup/stackup/coverletter/application/CoverLetterServiceTest.java b/backend/src/test/java/com/stackup/stackup/coverletter/application/CoverLetterServiceTest.java index ca5499d9..937d9358 100644 --- a/backend/src/test/java/com/stackup/stackup/coverletter/application/CoverLetterServiceTest.java +++ b/backend/src/test/java/com/stackup/stackup/coverletter/application/CoverLetterServiceTest.java @@ -78,4 +78,21 @@ private User user() { ReflectionTestUtils.setField(u, "id", 1L); return u; } + + // 자소서 본문은 S3 가 아니라 행 안에 산다 — 삭제 시 soft delete 만 하면 지원동기· + // 성장과정 원문이 그대로 남는다. cascade(분석물)와 별개로 원문 자체를 비워야 한다. + @Test + void delete_purgesInlineItemsAlongWithSoftDelete() { + User user = user(); + CoverLetter coverLetter = CoverLetter.create( + user, "공채 자소서", "[{\"question\":\"지원동기\",\"answer\":\"개인적 서술\"}]"); + ReflectionTestUtils.setField(coverLetter, "id", 7L); + when(coverLetterRepository.findByIdAndUser_IdAndDeletedFalse(7L, 1L)) + .thenReturn(Optional.of(coverLetter)); + + service.delete(1L, 7L); + + assertThat(coverLetter.isDeleted()).isTrue(); + assertThat(coverLetter.getItems()).isEqualTo("[]"); + } } diff --git a/docs/security.md b/docs/security.md index c9dbd38a..264f974f 100644 --- a/docs/security.md +++ b/docs/security.md @@ -167,6 +167,9 @@ public class GithubTokenCipher { 임베딩 청크는 DB 안에서 끝나므로 과거 삭제분도 V32 가 한 번에 정리한다. +자소서는 원문이 S3 가 아니라 **행 안에**(`cover_letters.items`) 산다 — 삭제 시 원문을 같은 +트랜잭션에서 빈 배열로 비우고(`CoverLetter.purgeItems`), 과거 삭제분은 V34 가 백필한다. + > 회원 탈퇴 시 사용자의 모든 자료를 일괄 파기하는 것은 별건이다(§5.3 의 hard delete, Phase 2). ### 5.2 민감정보