From 8a29045d22af794659cd97f66dec59797562c2aa Mon Sep 17 00:00:00 2001 From: MINYONG PARK Date: Fri, 7 Aug 2026 11:05:53 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EC=82=AD=EC=A0=9C=C2=B7=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=9D=B4=ED=9B=84=EC=9D=98=20=EA=B0=9C=EC=9D=B8?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=8C=8C=EA=B8=B0=20=EC=A0=95=EC=B1=85?= =?UTF-8?q?=EC=9D=84=20=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 본문은 소프트 삭제와 같은 트랜잭션에서 마스킹으로 즉시 파기하고, 행과 AI 파생 데이터는 90일 뒤 물리 삭제한다. 행동 이력은 30일에 식별자를 익명화하고 90일에 행을 지우며, 탈퇴 회원의 행은 기간과 무관하게 즉시 지운다. 06 §8의 "소프트 삭제 데이터 보존 기간" 미확정 항목을 해소하고, 물리 삭제 시점을 "별도 개인정보 정책"으로만 가리키던 06·08의 참조를 이 문서로 연결한다. Co-Authored-By: Claude Opus 5 --- README.md | 1 + ...7_\353\254\264\352\262\260\354\204\261.md" | 6 +- "static/08_API_\353\252\205\354\204\270.md" | 2 +- ...14\352\270\260\354\240\225\354\261\205.md" | 123 ++++++++++++++++++ 4 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 "static/13_\352\260\234\354\235\270\354\240\225\353\263\264_\355\214\214\352\270\260\354\240\225\354\261\205.md" diff --git a/README.md b/README.md index 38fe0e3..ba8840a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ PinLog은 장소를 `Context`와 함께 `Record`로 저장하고, 자연어로 | [MVP 기능범위](static/10_MVP_기능범위.md) | 포함·제외 기능 | | [인증 설계](static/11_인증_설계.md) | 인증 방식 결정과 근거, 클라이언트 계약 | | [배포 변수 및 Secret 표준](static/12_배포_변수_및_Secret_표준.md) | FE·BE·AI·Infra 변수/Secret owner, 전달 위치, 승인·rotation·rollback·rollout 계약 | +| [개인정보 파기 정책](static/13_개인정보_파기정책.md) | 삭제·탈퇴 이후의 파기 시점과 방식, 항목별 보유 기간 | ## 기준 diff --git "a/static/06_\353\215\260\354\235\264\355\204\260\353\252\250\353\215\270_\353\260\217_\353\254\264\352\262\260\354\204\261.md" "b/static/06_\353\215\260\354\235\264\355\204\260\353\252\250\353\215\270_\353\260\217_\353\254\264\352\262\260\354\204\261.md" index f45b37b..2cc0d7c 100644 --- "a/static/06_\353\215\260\354\235\264\355\204\260\353\252\250\353\215\270_\353\260\217_\353\254\264\352\262\260\354\204\261.md" +++ "b/static/06_\353\215\260\354\235\264\355\204\260\353\252\250\353\215\270_\353\260\217_\353\254\264\352\262\260\354\204\261.md" @@ -9,7 +9,7 @@ - 따라서 모든 유니크 제약은 **활성행(`deleted_at IS NULL`)에만 적용하는 부분 유니크 인덱스**로 정의합니다. 전체 유니크로 정의하면 삭제 후 재저장이 실패합니다. - 사용자 조회에서는 활성 데이터만 반환합니다. - Place는 공용 데이터이므로 삭제하지 않습니다. `deleted_at` 컬럼을 두지 않습니다. -- AI 파생 데이터(임베딩·키워드)는 물리 삭제하지 않고 **즉시 무효화 표시**합니다 — `ai.context_ai_state`의 두 status를 `CANCELLED`로 전이하고 `ai.context_embedding.is_deleted = true`를 표시합니다. `CANCELLED`가 진행 중인 AI 작업을 취소하고, `is_deleted`가 검색 제외와 물리 삭제 대상 식별을 담당합니다. 물리 삭제 시점은 별도 개인정보 정책을 따릅니다([AI 설계](05_AI_설계.md) §11). +- AI 파생 데이터(임베딩·키워드)는 물리 삭제하지 않고 **즉시 무효화 표시**합니다 — `ai.context_ai_state`의 두 status를 `CANCELLED`로 전이하고 `ai.context_embedding.is_deleted = true`를 표시합니다. `CANCELLED`가 진행 중인 AI 작업을 취소하고, `is_deleted`가 검색 제외와 물리 삭제 대상 식별을 담당합니다. 물리 삭제 시점은 [개인정보 파기 정책](13_개인정보_파기정책.md)을 따릅니다(무효화 후 90일, [AI 설계](05_AI_설계.md) §11). ### 1.2 기술 전제 @@ -566,7 +566,7 @@ Record 잠금 ``` - 연쇄 삭제 대상이 없어도 강제 삭제는 정상 수행합니다(일반 삭제와 동일 결과). -- AI 파생 데이터는 즉시 무효화 표시합니다. 소유는 AI 파트이나 트랜잭션 일관성을 위해 백엔드가 직접 씁니다(1.3 쓰기 매트릭스). 동일 인스턴스이므로 단일 트랜잭션으로 처리됩니다. 물리 삭제 시점은 별도 개인정보 정책을 따릅니다. +- AI 파생 데이터는 즉시 무효화 표시합니다. 소유는 AI 파트이나 트랜잭션 일관성을 위해 백엔드가 직접 씁니다(1.3 쓰기 매트릭스). 동일 인스턴스이므로 단일 트랜잭션으로 처리됩니다. 물리 삭제 시점은 [개인정보 파기 정책](13_개인정보_파기정책.md)을 따릅니다. ### 6.7 Collection에서 Record 제거 @@ -639,5 +639,5 @@ Collection 소프트 삭제 | ~~가입 미완료(동의 전) 상태 처리~~ | **확정**: 소셜 인증 성공 시점에 콜백에서 `member`·`social_account`를 생성한다. 필수 약관은 클라이언트가 로그인 시작 이전 화면에서 안내하므로 서버에 가입 미완료 상태가 존재하지 않는다(API 명세 3.2) | | ~~Context 본문 최대 길이~~ | **확정: 500자.** 초과 요청은 `400 INVALID_INPUT`으로 거절한다. 임베딩 모델(`text-embedding-3-small`)은 8191토큰까지 받으므로 모델 제약이 아니라 비용과 UX 기준으로 정한 값이다. 본문이 들어오는 경로 셋(Record 생성의 `contextBody`, Context 추가·수정의 `body`)에 모두 적용한다. 프론트는 입력 UI에서 같은 값으로 막는다([파트 간 요구사항](05-1_파트간_요구사항.md) §1.5) | | `is_published`의 MVP 활용 여부 | 비공개 전환을 제공하면 Feed·타인 Shelf 조회·Collection 상세 세 경로에 필터 필요. `record_count`의 정의(전체 vs 공개분)도 결정 필요 | -| 소프트 삭제 데이터 보존 기간 | 미정 시 `collection_record`, `follow`가 무한 증식. 하드 삭제 배치 정책 필요 | +| ~~소프트 삭제 데이터 보존 기간~~ | **확정: 90일.** 소프트 삭제 후 90일이 지난 행을 물리 삭제하고, `context.body`는 그 전에 소프트 삭제와 같은 트랜잭션에서 마스킹으로 파기한다([개인정보 파기 정책](13_개인정보_파기정책.md)) | | `ai.context_keyword.confidence` 활용 | 추천 가중치로 사용할지 여부 | diff --git "a/static/08_API_\353\252\205\354\204\270.md" "b/static/08_API_\353\252\205\354\204\270.md" index 1c6c96d..f2f44e0 100644 --- "a/static/08_API_\353\252\205\354\204\270.md" +++ "b/static/08_API_\353\252\205\354\204\270.md" @@ -782,7 +782,7 @@ DELETE /api/core/v1/records/{recordId} ``` - 프론트는 이 Record가 어떤 Collection의 마지막 Record인지 알 수 없다. 서버가 DB에서 확인한다. -- 마지막 Record인 활성 Collection이 없으면: Record·Context 소프트 삭제, Collection 연결 소프트 삭제, AI 파생 데이터 무효화 — State `CANCELLED` + Embedding `is_deleted`(204). 물리 삭제 시점은 미결이며 별도 개인정보 정책을 따른다([06 §1.1](06_데이터모델_및_무결성.md)). +- 마지막 Record인 활성 Collection이 없으면: Record·Context 소프트 삭제, Collection 연결 소프트 삭제, AI 파생 데이터 무효화 — State `CANCELLED` + Embedding `is_deleted`(204). 물리 삭제 시점은 [개인정보 파기 정책](13_개인정보_파기정책.md)을 따른다(무효화 후 90일, [06 §1.1](06_데이터모델_및_무결성.md)). - 마지막 Record인 활성 Collection이 있으면: 삭제하지 않고 409로 거절한다. ```http diff --git "a/static/13_\352\260\234\354\235\270\354\240\225\353\263\264_\355\214\214\352\270\260\354\240\225\354\261\205.md" "b/static/13_\352\260\234\354\235\270\354\240\225\353\263\264_\355\214\214\352\270\260\354\240\225\354\261\205.md" new file mode 100644 index 0000000..077f29a --- /dev/null +++ "b/static/13_\352\260\234\354\235\270\354\240\225\353\263\264_\355\214\214\352\270\260\354\240\225\354\261\205.md" @@ -0,0 +1,123 @@ +# PinLog 개인정보 파기 정책 + +## 1. 적용 범위 + +이 문서는 **삭제·탈퇴 이후의 데이터 취급**을 정합니다. 다른 사용자에게 무엇이 보이는지는 [익명 SNS 공개정책](04_익명SNS_공개정책.md), 삭제 트랜잭션의 순서와 연쇄 범위는 [데이터 모델](06_데이터모델_및_무결성.md) 6장이 정합니다. + +[데이터 모델](06_데이터모델_및_무결성.md) 1.1·6.6과 [AI 설계](05_AI_설계.md) 11.5가 "물리 삭제 시점은 별도 개인정보 정책을 따른다"고 가리키는 문서가 이 문서입니다. + +### 1.1 원칙 + +1. **원문은 즉시 파기합니다.** 사용자가 지운 서술은 삭제 트랜잭션 안에서 복원할 수 없는 값으로 덮어씁니다. +2. **행은 유예 후 물리 삭제합니다.** 소프트 삭제로 조회에서 즉시 빠진 행은 90일 뒤에 지웁니다. +3. **파기는 되돌릴 수 없어야 합니다.** 마스킹은 치환이며 `NULL`로 만드는 것이 아니고, 치환값으로 원본을 유추할 수 없어야 합니다([데이터 모델](06_데이터모델_및_무결성.md) 2.2와 같은 규약). + +### 1.2 파기를 유예할 외부 근거가 없습니다 + +PinLog는 결제·계약·청약철회 기록을 다루지 않으므로, 법령상 보존 의무 때문에 남겨야 하는 항목이 하나도 없습니다. 보유 기간은 전부 서비스 자신의 목적으로만 정해지며, 목적이 사라지면 남길 이유가 없습니다. + +## 2. 항목별 파기 기준 + +| 항목 | 개인정보성 | 파기 시점 | 방식 | 소유 | +|---|---|---|---|---| +| `social_account.provider_user_id` · `email` | 직접 식별 | 탈퇴 **즉시** | 치환 마스킹 | 백엔드 | +| `context.body` | 본인 서술 | 소프트 삭제 **즉시** | 치환 마스킹 | 백엔드 | +| `context` 행 | — | 소프트 삭제 후 **90일** | 물리 삭제 | 백엔드 | +| `record` · `collection` · `collection_record` · `follow` 행 | 준식별(장소·관계 이력) | 소프트 삭제 후 **90일** | 물리 삭제 | 백엔드 | +| `ai.context_embedding` · `ai.context_keyword` · `ai.context_ai_state` · `ai.context_keyword_analysis` | 본문 파생 | 무효화 즉시 + **90일** | 물리 삭제 | AI | +| `core.feed_event`의 `member_id` | 행동 이력 | **30일** | 익명화 | AI | +| `core.feed_event` 행 | 집계 데이터 | **90일** | 물리 삭제 | AI | +| `core.feed_event` 중 탈퇴 회원의 행 | 행동 이력 | 탈퇴 **즉시** | 물리 삭제 | AI | +| `core.place` | 없음 | 파기하지 않음 | — | 백엔드 | + +소유는 [데이터 모델](06_데이터모델_및_무결성.md) 1.3의 스키마 소유와 쓰기 매트릭스를 따릅니다. `ai` 스키마와 `core.feed_event`의 파기 실행은 AI 파트가 담당합니다. + +## 3. 원문 즉시 파기 + +대상은 `context.body`이며, **맥락이 소프트 삭제되는 모든 경로**에 적용합니다. + +| 경로 | 근거 | +|---|---| +| 맥락 삭제 | [데이터 모델](06_데이터모델_및_무결성.md) 6.5 | +| 맥락 수정 — 교체 생성으로 밀려난 구 맥락 | [데이터 모델](06_데이터모델_및_무결성.md) 6.4 | +| 기록 삭제 — 그 기록의 맥락 전체 | [데이터 모델](06_데이터모델_및_무결성.md) 6.6 | +| 회원 탈퇴 | [데이터 모델](06_데이터모델_및_무결성.md) 6.9 | + +- **수정 경로가 건수가 가장 많습니다.** Context는 불변 엔티티이므로 수정이 교체 생성으로 표현되고, 그 결과 사용자가 고쳐 쓴 문장이 구 맥락으로 계속 쌓입니다. 원문 파기를 삭제·탈퇴로만 한정하면 실제로 가장 많이 남는 것이 그대로 남습니다. +- **치환값이 필요합니다.** `ck_context_body`가 공백만인 본문을 금지하므로 `NULL`도 빈 문자열도 넣을 수 없습니다. 구체적 치환값은 구현이 정하며, 이 문서가 정하는 것은 원본을 복원할 수 없어야 한다는 것입니다. +- **마스킹과 소프트 삭제는 한 메서드에 담습니다.** 호출부마다 따로 부르면 나중에 생기는 경로에서 빠지고, 그 누락이 곧 파기 실패입니다. `SocialAccount`의 탈퇴 처리가 같은 규약으로 되어 있습니다. +- 파기된 뒤에는 복구 경로가 없습니다. 사용자 복구를 제공하지 않는 정책([정책 정의서](02_정책_정의서.md) 1.5)과 어긋나지 않습니다. + +## 4. 90일 유예 후 물리 삭제 + +### 4.1 대상과 기준 시각 + +| 대상 | 기준 시각 | +|---|---| +| `context` · `record` · `collection` · `collection_record` · `follow` | `deleted_at` | +| `ai.context_ai_state` · `ai.context_embedding` · `ai.context_keyword` · `ai.context_keyword_analysis` | 무효화 시각(`context_ai_state.updated_at`) | + +### 4.2 유예를 두는 이유 + +원문은 이미 3장에서 파기된 상태이므로, 이 90일 동안 남아 있는 것은 **개인의 서술이 아니라 파생값과 관계 이력**입니다. 파생 데이터는 프리셋 보정과 집계 재현에 쓰일 여지가 있고([AI 설계](05_AI_설계.md) 8.5·8.6), 그 판단은 원문 보관과 성질이 다릅니다. + +### 4.3 삭제 순서 + +자식에서 부모 방향으로 지웁니다. + +```text +ai 파생 4종 (core를 참조하지 않으므로 core와 순서 제약이 없습니다) +context → collection_record → record · collection → follow +``` + +- 대상은 활성행이 아니라 `deleted_at`이 90일을 지난 행입니다. 유니크 제약은 모두 활성행 부분 유니크이므로([데이터 모델](06_데이터모델_및_무결성.md) 3.1) 삭제분을 지우는 것이 제약과 집계에 영향을 주지 않습니다. +- 물리 삭제 이후에는 운영자도 복구할 수 없습니다. "복구가 필요하면 데이터를 읽어 새로 INSERT한다"는 규약([데이터 모델](06_데이터모델_및_무결성.md) 7장)과 어긋나지 않습니다. + +## 5. 행동 이력 + +`core.feed_event`는 삭제열이 없는 추가 전용 관측 로그입니다. 두 단계로 처리합니다. + +| 시점 | 처리 | +|---|---| +| 30일 | `member_id`를 회원을 특정할 수 없는 고정값으로 치환합니다. 컬럼이 `NOT NULL`이므로 `NULL`을 쓰지 않습니다 | +| 90일 | 행을 물리 삭제합니다 | +| 회원 탈퇴 | 기간과 무관하게 그 회원의 행을 즉시 물리 삭제합니다 | + +- **30일 이후의 `member_id`는 어떤 기능에도 쓰이지 않습니다.** 노출 감점 집계 창이 7일이므로 그 창 밖의 행은 점수 계산에 들어가지 않습니다. 익명화 이후의 행은 개인을 지목할 수 없는 집계 데이터가 됩니다. +- **90일까지 행을 남기는 것은 집계 목적입니다.** 추천 성과(`CLICK`·`SAVE`) 산정과 집계 창 값 조정의 재현에 쓰며, 개인 경험을 위해 남기는 것이 아닙니다. 창 값과 보존 기간을 같게 두면 창을 늘리는 조정을 검증할 데이터가 사라집니다. +- **탈퇴 시 즉시 삭제하는 이유는 목적 소멸입니다.** 감점 계산은 그 회원이 다시 요청할 때만 의미가 있습니다. "대상 Collection이 삭제된 뒤에도 집계용으로 남아야 한다"는 원래 설계 근거는 대상에 관한 것이며 요청자 탈퇴와 충돌하지 않습니다. +- 대상 Collection이 삭제된 뒤에도 `collection_id`가 남는 것은 정상입니다. 이 테이블에 외래키를 두지 않은 이유가 그것입니다. + +## 6. 파기하지 않는 것 + +- **`core.place`** — 여러 회원의 Record가 함께 참조하는 공용 데이터이며 개인 소유가 아닙니다. 회원이 탈퇴해도 유지합니다([정책 정의서](02_정책_정의서.md) 3장). +- **익명화된 `core.feed_event` 행** — 개인을 지목할 수 없으므로 개인정보가 아니며, 90일 삭제 대상으로만 남습니다. +- **`ai.keyword_preset`** — 서비스가 정의한 프리셋 목록이며 사용자 데이터가 아닙니다. + +## 7. 백업과 로그 + +| 대상 | 보존 | 파기 방식 | +|---|---|---| +| PostgreSQL 덤프 | 7일 | 매일 1회 생성하고 7일이 지난 파일을 삭제합니다 | +| 애플리케이션 로그 | 72시간 | 보존 기간이 지난 로그를 삭제합니다 | + +- 파기한 원문은 **최대 7일 안에 백업본에서도 사라집니다.** 순환 주기가 곧 백업의 보존 기간입니다. +- 백업과 로그에서 개별 행을 골라 지우지 않습니다. 파기 요청 시점에 서비스 데이터베이스에서 파기하고, 백업본은 순환으로 소멸시킵니다. +- 로그에는 개인정보를 남기지 않습니다. 본문·이메일·토큰을 기록하지 않으며, AI 처리의 요청 본문에는 사용자 서술이 들어 있으므로 요청·응답 본문을 로그에 싣지 않습니다([AI 설계](05_AI_설계.md) 13.3). + +## 8. 이용자 안내에 담아야 하는 것 + +- 수집 항목 — 이메일, 소셜 공급자 식별자, 사용자가 작성한 맥락 본문, 저장한 장소와 컬렉션 구성. +- 보유 기간 — 2장의 표. 항목별로 기간을 밝히며 "필요한 기간"처럼 뭉개지 않습니다. +- 파기 방법 — 원문은 복원할 수 없는 값으로 치환, 행은 물리 삭제. +- **AI 분석을 위해 작성한 내용이 외부 모델 서비스로 전송된다는 사실.** 전송 대상은 맥락 본문과 장소 메타이며 이메일·소셜 식별자는 전송하지 않습니다([AI 설계](05_AI_설계.md) 13장). +- 자동 발행과 공개 범위 안내는 [익명 SNS 공개정책](04_익명SNS_공개정책.md) 3장을 따릅니다. + +## 9. 관련 문서 + +| 문서 | 이 문서와의 관계 | +|---|---| +| [정책 정의서](02_정책_정의서.md) | 삭제·탈퇴 정책의 상위 규칙 | +| [익명 SNS 공개정책](04_익명SNS_공개정책.md) | 살아 있는 데이터의 공개 범위 | +| [AI 설계](05_AI_설계.md) | AI 파생 데이터의 무효화 장치(11장)와 외부 전송 경계(13장) | +| [데이터 모델 및 무결성](06_데이터모델_및_무결성.md) | 삭제 트랜잭션의 순서와 연쇄 범위(6장), 스키마 소유(1.3) |