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) |