---
template: policy-doc@1
doc_id: 01-retention
title: 데이터 보존 — 자동 삭제 배치 4종의 대상과 창
config: ../_meta/template-config.yaml
last_verified: 4d798f55
policies:
  - id: retention:delete-analysis
    status: conditional-unknown
    evidence: [A-scheduler, A-comment]
    unknown_reason: 이 규칙의 발효는 담당 배치의 활성에 걸려 있고 그 활성은 판정되지 않았다 — 04-runtime-topology.md#schedule:delete-analysis 가 그 미판정의 근거와 남은 입력을 진다. 삭제 창 자체는 동결 stg 4d798f55 의 코드 행으로 확정된다.
    anchors:
      - insuniverse-server/apps/api/src/schedules/delete-analysis.schedule.ts:22
      - insuniverse-server/apps/api/src/schedules/delete-analysis.schedule.ts:23
      - insuniverse-server/apps/api/src/schedules/delete-analysis.schedule.ts:24
      - insuniverse-server/apps/api/src/modules/analyze/analyze.repository.ts:580
      - insuniverse-server/apps/api/src/modules/analyze/analyze.repository.ts:583
      - insuniverse-server/apps/api/src/modules/client/analyze/client-analyze.repository.ts:570
      - insuniverse-server/apps/api/src/modules/client/analyze/client-analyze.repository.ts:573
  - id: retention:delete-fc
    status: conditional-unknown
    evidence: [A-scheduler, A-comment]
    unknown_reason: 이 규칙의 발효는 담당 배치의 활성에 걸려 있고 그 활성은 판정되지 않았다 — 04-runtime-topology.md#schedule:delete-fc 가 그 미판정의 근거와 남은 입력을 진다. 여기에 더해 진입 형태 때문에 활성이 확인되더라도 삭제가 수행되는 지점이 없다(limitations).
    limitations:
      - 탈퇴 후 1년이 지난 회원 데이터의 삭제가 동결 stg 4d798f55 의 소스 수준에서 수행되지 않는다. 진입 파일 `apps/api/src/schedules/delete-fc.schdule.ts` 최상위가 `async () => { … };` 함수 표현식이고 그것을 부르는 구문이 저장소 안에 없다 — 배치 17종 중 진입 호출 증거를 갖는 16종은 `})();` 즉시 실행 함수 · `@Cron` · `require.main` 명명 호출 셋 중 하나로 진입하고, 증거가 없는 것은 이 1종뿐이다. 개인정보 보유 기간을 이 문서로 판단할 때 이 사실이 창(1년)보다 앞선다
      - 삭제 질의 자체(`fc.repository.ts` `delete`)는 1년 창으로 존재하며 다른 호출 경로에서 도달할 수 있는지는 이 창이 판정하지 않았다
    anchors:
      - insuniverse-server/apps/api/src/schedules/delete-fc.schdule.ts:19
      - insuniverse-server/apps/api/src/modules/fc/fc.repository.ts:583
      - insuniverse-server/apps/api/src/modules/fc/fc.repository.ts:587
  - id: retention:delete-invoice-files
    status: conditional-unknown
    evidence: [A-scheduler, A-comment]
    unknown_reason: 이 규칙의 발효는 담당 배치의 활성에 걸려 있고 그 활성은 판정되지 않았다 — 04-runtime-topology.md#schedule:delete-invoice-files 가 그 미판정의 근거와 남은 입력을 진다. 삭제 창 자체는 동결 stg 4d798f55 의 코드 행으로 확정된다.
    limitations:
      - S3 객체 삭제가 실패해도 그 파일만 실패로 세고 다음 파일 처리를 계속한다(`delete-invoice-files.schedule.ts:85` 주석). 그래서 DB 의 S3 키가 비워진 뒤에도 객체가 남아 있을 수 있고, 이 문서는 그 잔존 여부를 판정하지 않는다
    anchors:
      - insuniverse-server/apps/api/src/schedules/delete-invoice-files.schedule.ts:33
      - insuniverse-server/apps/api/src/schedules/delete-invoice-files.schedule.ts:79
      - insuniverse-server/apps/api/src/schedules/delete-invoice-files.schedule.ts:90
      - insuniverse-server/apps/api/src/schedules/delete-invoice-files.schedule.ts:205
      - insuniverse-server/apps/api/src/modules/invoice/invoice.repository.ts:599
      - insuniverse-server/apps/api/src/modules/invoice/invoice.repository.ts:603
  - id: retention:delete-treat
    status: conditional-unknown
    evidence: [A-scheduler, A-comment]
    unknown_reason: 이 규칙의 발효는 담당 배치의 활성에 걸려 있고 그 활성은 판정되지 않았다 — 04-runtime-topology.md#schedule:delete-treat 가 그 미판정의 근거와 남은 입력을 진다. 삭제 창 자체는 동결 stg 4d798f55 의 코드 행으로 확정된다.
    anchors:
      - insuniverse-server/apps/api/src/schedules/delete-treat.schedule.ts:23
      - insuniverse-server/apps/api/src/schedules/delete-treat.schedule.ts:24
      - insuniverse-server/apps/api/src/schedules/delete-treat.schedule.ts:25
      - insuniverse-server/apps/api/src/modules/treat/treat.repository.ts:446
      - insuniverse-server/apps/api/src/modules/client/client/client.repository.ts:600
      - insuniverse-server/apps/api/src/modules/client/client/client.repository.ts:604
---

# 데이터 보존 — 자동 삭제 배치 4종의 대상과 창

*"어떤 데이터가 언제 자동으로 삭제되는가"* 의 답을 저장소 안에서 찾은 결과다. 대상 데이터와
보존 창은 **동결 stg `4d798f55` 트리의 코드 행**이 정하고, 이 문서는 그 행의 원문을 규칙과
나란히 싣는다. 창의 값은 이 문서의 주장이 아니라 그 트리에서 다시 읽히는 사실이다 —
`## 근거 대조표` 가 그 대조를 담는다.

삭제 규칙은 넷이고 배치도 넷이다. `04-runtime-topology.md` 가 확정한 것은 이 배치들의 *선언*
이었고 **무엇을 지우는지는 그 문서가 적지 않았다.** 이 문서가 그 자리를 채운다. 반대로 배치가
지금 실제로 도는지는 이 문서도 답하지 않으며, 그 판정에 필요한 입력이 무엇인지는 각 정책의
`unknown_reason` 이 M1 항목을 이름으로 가리켜 넘긴다.

수치 상수의 *정의* 축은 `30-contracts-limits` 소유다. 여기 실린 값은 상수 선언이 아니라 지역
호출의 인라인 인자이며, 이 문서는 그 값의 **의미**만 해석한다. 삭제 질의가 부르는 도메인 로직
(연쇄 삭제 순서·트랜잭션 경계)은 `50-domain` 소유다.

## 정책 (기획)

**이 절에는 정책 블록이 없다. 보존 규칙 넷 전부가 `## 부록` 에 있다.**

보존 규칙은 *언제 지워지는가* 가 아니라 **언제 지워지기 시작하는가** 로 읽힌다. 창이 30일이라는
사실과 30일 된 데이터가 실제로 사라진다는 사실은 다르고, 둘을 가르는 것은 담당 배치가 그 시각에
호출되는가다. 그 호출은 EventBridge 규칙과 ECS task-def 가 정하며 **둘 다 저장소 밖에 있다.**

그래서 규칙만 본문에 올리면 독자는 *지금 걸려 있는 보존 정책* 으로 읽는다. 개인정보 보유 기간을
이 문서로 판단하는 독자에게 그 오독의 대가가 가장 크므로, 창의 값이 코드로 확정된 것과 무관하게
넷 다 부록에 둔다. 본문이 채워지는 시점은 사람의 결심이 아니라 그 두 입력의 확보가 정한다.

## 근거 대조표

각 행은 규칙 값 하나에 대한 **코드 측 주장과 주석 측 주장**을 나란히 싣는다. 두 원문은 동결
stg `4d798f55` 트리의 그 행 그대로이고, `기준값` 은 코드 측 정규 쌍의 표준 표기(코드 측이 없으면
주석 측)이며, `판정` 은 게이트가 두 원문에서 다시 유도해 계산한다. 행 집합은 이 문서가 고르지
않는다 — 담당 스케줄러의 docblock 수량행과 삭제 질의 심볼 스팬에서 유도된 것 전부다.

| 정책 id | 기준값 | 코드 앵커 | 코드 원문 | 주석 앵커 | 주석 원문 | 판정 |
|---|---|---|---|---|---|---|
| `retention:delete-analysis` | 30일 | `insuniverse-server/apps/api/src/modules/analyze/analyze.repository.ts:583` | `    const subDate = nowDate.subtract(30, 'day').format('YYYY-MM-DD HH:mm:ss');` | `insuniverse-server/apps/api/src/modules/analyze/analyze.repository.ts:580` | `  //* 스케줄러를 통한 30일 경과시 분석결과 데이터 삭제(기본, 상세, 자동차진료기본, 자동차진료상세)` | 일치 |
| `retention:delete-analysis` | 30일 | `insuniverse-server/apps/api/src/modules/client/analyze/client-analyze.repository.ts:573` | `    const subDate = nowDate.subtract(30, 'day').format('YYYY-MM-DD HH:mm:ss');` | `insuniverse-server/apps/api/src/modules/client/analyze/client-analyze.repository.ts:570` | `  //* 스케줄러를 통한 30일 경과시 분석결과 데이터 삭제(기본, 상세, 자동차진료기본, 자동차진료상세)` | 일치 |
| `retention:delete-analysis` | 30일 | — | — | `insuniverse-server/apps/api/src/schedules/delete-analysis.schedule.ts:10` | ` * 분석결과정보의 생성일이 30일이 지난 경우 분석결과정보 데이터를 삭제합니다.` | 코드 없음 |
| `retention:delete-fc` | 1년 | `insuniverse-server/apps/api/src/modules/fc/fc.repository.ts:587` | `    const subDate = nowDate.subtract(1, 'year').format('YYYY-MM-DD HH:mm:ss');` | `insuniverse-server/apps/api/src/modules/fc/fc.repository.ts:583` | `  //* 스케줄러를 통한 탈퇴후 1년 경과시 회원 정보 삭제` | 일치 |
| `retention:delete-invoice-files` | 30일 | `insuniverse-server/apps/api/src/modules/invoice/invoice.repository.ts:603` | `      .subtract(30, 'day')` | `insuniverse-server/apps/api/src/modules/invoice/invoice.repository.ts:599` | `   * 30일 이상 경과된 청구서 조회 (S3 파일이 있는 경우만)` | 일치 |
| `retention:delete-invoice-files` | 30일 | — | — | `insuniverse-server/apps/api/src/schedules/delete-invoice-files.schedule.ts:13` | ` * ivh_created_at 기준 30일이 지난 청구서의 S3 파일을 삭제하고` | 코드 없음 |
| `retention:delete-treat` | 30일 | `insuniverse-server/apps/api/src/modules/client/client/client.repository.ts:604` | `      .subtract(30, 'day')` | `insuniverse-server/apps/api/src/modules/client/client/client.repository.ts:600` | `  //* 스케줄러를 통한 임시데이터 삭제 (생성일이 30일이 경과된 경우)` | 일치 |
| `retention:delete-treat` | 30일 | `insuniverse-server/apps/api/src/modules/treat/treat.repository.ts:446` | `    const subDate = dayjs().subtract(30, 'day').format('YYYY-MM-DD HH:mm:ss');` | — | — | 주석 없음 |
| `retention:delete-treat` | 30일 | — | — | `insuniverse-server/apps/api/src/schedules/delete-treat.schedule.ts:10` | ` * 주문상세(분석)의 생성일이 30일이 지난 경우 심평원의 원본데이터를 삭제합니다.` | 코드 없음 |

**이 표의 네 규칙은 코드와 주석이 갈리지 않는다.** 갈리는 자리는 감시 축에 둘 있고
`03-monitoring.md` 의 같은 표가 그것을 담는다. 갈리지 않는 자리를 표에서 빼면 표본이 범위인
척하게 되므로, 여기서는 일치하는 행도 남김없이 싣는다.

`delete-fc` 의 docblock(`apps/api/src/schedules/delete-fc.schdule.ts:9`)은 *탈퇴 이후 일년이 지난 경우* 라고 적는데 이
표에 행이 없다. 한글 수사 표기는 기계 유도 집합 밖이라 게이트가 그 행을 짝지어 주지 못하기
때문이다 — 그 사실 자체는 게이트가 매 실행 건수로 찍는다.

## 강제 지점 (개발)

규칙 내용을 다시 쓰지 않는다. 각 정책이 저장소 안 **어디에서** 강제되는가와 그 지점이 어떤
기제인가만 적는다. 배치가 부록인 정책도 이 절에 있다.

### retention:delete-analysis

- 강제 방식: 리포지터리 삭제 질의. `insuniverse-server/apps/api/src/modules/analyze/analyze.repository.ts:583` 과 `insuniverse-server/apps/api/src/modules/client/analyze/client-analyze.repository.ts:573` 두 곳이 창을 계산해 대상을 고른다.
- 진입: `insuniverse-server/apps/api/src/schedules/delete-analysis.schedule.ts:22` · `:23` · `:24` 세 호출이 심평원 · 고객사 · B2C 세 갈래를 차례로 부른다.
- 기준점: 동결 stg `4d798f55` 에서만 해석했다. 미동결 prod 스냅샷은 이 문서의 근거가 아니다.

### retention:delete-fc

- 강제 방식: 리포지터리 삭제 질의. `insuniverse-server/apps/api/src/modules/fc/fc.repository.ts:587` 이 창을 계산한다.
- 진입: `insuniverse-server/apps/api/src/schedules/delete-fc.schdule.ts:19` 이 유일한 호출이며, 그 호출을 감싼 최상위 함수 표현식을 부르는 구문이 저장소 안에 없다. `limitations` 가 그 사실을 진다.
- 기준점: 동결 stg `4d798f55`.

### retention:delete-invoice-files

- 강제 방식: 만료 조회 + S3 객체 삭제 + DB 키 비우기 세 단계. `insuniverse-server/apps/api/src/modules/invoice/invoice.repository.ts:603` 이 창을, `insuniverse-server/apps/api/src/schedules/delete-invoice-files.schedule.ts:79` 이 객체 삭제를, 동 `:90` 이 키 비우기를 맡는다.
- 진입: `insuniverse-server/apps/api/src/schedules/delete-invoice-files.schedule.ts:205` 이 배치 본문을 부르고 `:33` 이 대상 목록을 읽는다. 이 배치만 전용 Nest 모듈을 따로 두고 앱 전체 모듈을 올리지 않는다.
- 기준점: 동결 stg `4d798f55`.

### retention:delete-treat

- 강제 방식: 리포지터리 삭제 질의. `insuniverse-server/apps/api/src/modules/treat/treat.repository.ts:446` 과 `insuniverse-server/apps/api/src/modules/client/client/client.repository.ts:604` 두 곳이 창을 계산한다.
- 진입: `insuniverse-server/apps/api/src/schedules/delete-treat.schedule.ts:23` · `:24` · `:25` 세 호출이 심평원 · 고객사 · B2C 세 갈래를 차례로 부른다.
- 기준점: 동결 stg `4d798f55`.

## 적용 범위 (actor × 서비스)

보존 규칙에는 actor 표면이 없다. 이 문서의 정책은 사람이 어떤 화면에서 무엇을 할 수 있는가가
아니라 **서버가 데이터를 지우는 조건**에 대한 사실이며, 그 사실은 어느 actor 의 요청 경로에도
등장하지 않는다. actor 다섯 열이 전건 `—` 인 것은 빈 칸이 아니라 관측된 부재다. 서비스 축에서는
`server` 하나만 `적용` 이다 — `site` 와 `admin` 은 이 삭제를 호출하지도 관측하지도 않는다.

| 정책 id | 관리자 | 영업대리점 | 기업담당자 | 일반회원 | 기업회원 | server | site | admin |
|---|---|---|---|---|---|---|---|---|
| retention:delete-analysis | — | — | — | — | — | 적용 | — | — |
| retention:delete-fc | — | — | — | — | — | 적용 | — | — |
| retention:delete-invoice-files | — | — | — | — | — | 적용 | — | — |
| retention:delete-treat | — | — | — | — | — | 적용 | — | — |

## 예외·폴백

예외가 없는 정책도 행을 둔다 — 없다는 것도 확인의 결과이며 그 확인 근거를 함께 적는다.

| 정책 id | 예외 | 폴백 | 확인 근거 |
|---|---|---|---|
| retention:delete-analysis | 관측된 예외 없음 | 관측된 폴백 없음 | `delete-analysis.schedule.ts` 최상위 세 호출에 `catch` 절이 없다. 한 갈래가 던지면 남은 갈래가 그 실행에서 수행되지 않는다 |
| retention:delete-fc | 관측된 예외 없음 | 관측된 폴백 없음 | 진입 함수 표현식 안에 오류 처리가 없다. 진입 자체의 미호출은 `limitations` 소관이다 |
| retention:delete-invoice-files | 파일 키가 하나도 없는 청구서는 건너뛰고 `skippedCount` 로 센다 | S3 객체 삭제 실패 시 그 파일만 실패로 세고 같은 청구서의 다음 파일을 계속 처리한다 (`apps/api/src/schedules/delete-invoice-files.schedule.ts:85`) | 청구서 단위 `try`/`catch` 와 파일 단위 `try`/`catch` 가 중첩돼 있다 |
| retention:delete-treat | 관측된 예외 없음 | 관측된 폴백 없음 | `delete-treat.schedule.ts` 최상위 세 호출에 `catch` 절이 없다. 같은 파일 27~36행의 이전 구현은 주석 처리돼 있어 강제 지점이 아니다 |

## 부록

이 절의 정책은 **현행 발효 여부가 판정되지 않아** 본문에서 격리한다 — 판정에 필요한 입력
(EventBridge 규칙 · ECS task-def)이 저장소 밖에 있고, 그 상태로 본문에 두면 독자가 지금 걸려
있는 보존 정책으로 읽는다.

**여기 실린 어떤 정책도 배치의 활성을 부정하지 않는다.** 미판정과 부정은 다르다. 저장소는
활성을 긍정할 근거도 부정할 근거도 갖고 있지 않다. 삭제 배치는 개인정보 보유 기간 판단에
직접 걸리므로 어느 방향으로든 단정하면 위험하다.

각 블록은 **규칙의 조건과 결과**를 코드 심볼 없이 적는다 — 무엇이 대상인가 · 창은 얼마인가 ·
그래서 사용자에게 무엇이 일어나는가. 좌표는 프론트매터 `anchors` 와 `## 근거 대조표` 가 진다.

### retention:delete-analysis

분석결과 정보가 만들어진 지 **30일**이 지나면 그 분석결과 데이터를 지운다. 대상은 세 갈래다 —
심평원 분석결과, 고객사(기업) 분석결과, B2C 분석결과. 셋을 한 배치가 차례로 처리한다.

사용자에게 일어나는 일: 30일이 지난 분석 건은 결과 화면에서 내용을 되찾을 수 없다. 주문상세의
껍데기가 남는지 함께 사라지는지는 이 문서가 판정하지 않는다 — 그 경계는 `50-domain` 소관이다.

발효는 담당 배치의 활성에 걸려 있고 그 활성은 미판정이다. 남은 입력은
`04-runtime-topology.md#schedule:delete-analysis` 가 진다.

### retention:delete-fc

탈퇴한 회원의 정보를 탈퇴 후 **1년**이 지나면 지운다.

**진입 형태 때문에 이 규칙은 소스 수준에서 수행되는 지점이 없다.** 배치 17종 가운데 진입 호출
증거를 갖는 것이 16종이고 갖지 못한 것은 이 배치 1종뿐이다. 증거의 형태는 하나가 아니다 —
`})();` 즉시 실행 함수가 다수이고, `push.scheduler` 는 `@Cron` 데코레이터로,
`samsung-s2-report` 는 `require.main === module` 아래의 명명 호출로 진입한다. 이 파일만 최상위
함수 표현식을 정의만 하고 끝나서 어느 형태에도 들지 않는다. 컨테이너가 이 파일을
집어 실행해도 그 안의 삭제 호출에 도달하지 않는다. 이것은 활성 판정과 **별개의 층위**이며 —
활성이 확인되더라도 이 사실은 그대로다 — 개인정보 보유 기간을 이 문서로 판단할 때 창(1년)보다
앞서 읽어야 한다.

삭제 질의 자체는 1년 창으로 존재한다. 그 질의가 다른 호출 경로에서 도달하는지는 이 창이
판정하지 않았다.

발효 판정에 남은 입력은 `04-runtime-topology.md#schedule:delete-fc` 가 진다.

### retention:delete-invoice-files

청구서가 만들어진 지 **30일**이 지나면 그 청구서에 붙은 S3 파일을 지우고 DB 의 파일 키 두 칸을
비운다. 대상은 파일 키가 실제로 있는 청구서뿐이고, 키가 없는 건은 건너뛴 것으로 센다.

사용자에게 일어나는 일: 30일이 지난 청구서의 첨부와 본문 파일을 더는 내려받을 수 없다. 청구서
레코드 자체는 남는다 — 지우는 것은 파일과 그 키다.

이 배치는 앱 전체 모듈을 올리지 않고 전용 최소 모듈을 쓴다. 그래서 다른 배치와 의존 범위가
다르며, 그 차이가 활성 판정의 입력을 바꾸지는 않는다.

발효 판정에 남은 입력은 `04-runtime-topology.md#schedule:delete-invoice-files` 가 진다.

### retention:delete-treat

주문상세(분석)가 만들어진 지 **30일**이 지나면 심평원에서 받아 온 원본 임시데이터를 지운다.
대상은 세 갈래다 — 심평원 원본, 고객사(기업) 원본, B2C 원본.

사용자에게 일어나는 일: 30일이 지난 건은 원본 자료를 다시 열 수 없다. 그 원본에서 만들어진
분석결과는 `retention:delete-analysis` 가 같은 30일 창으로 따로 지운다 — 두 규칙은 대상이 다르고
배치도 다르다.

발효 판정에 남은 입력은 `04-runtime-topology.md#schedule:delete-treat` 가 진다.

## 연혁

릴리스 태그가 존재하지 않는다. 릴리스 열은 `—` 로 두고 커밋 SHA 로 추적한다. 행은
`docs/_meta/index/commits.server.jsonl` 이 이 문서의 앵커 경로 접촉으로 기록한 커밋 집합 안에서
**보존 규칙이 바뀐 시점**만 골랐다. 그 집합 전체(235건)를 싣지 않는다 — 이 계수는 문서의
주장이 아니라 게이트 `#9b` 가 인덱스에서 다시 세는 값이다.

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2025-01-07 | `f77980f0` | 심평원 원본임시데이터 삭제 배치 파일 신설 |
| — | 2025-03-25 | `0a036d0d` | 분석결과 삭제 배치 신설 — 이 시점의 창은 1년 |
| — | 2025-03-28 | `a2b3ba40` | 탈퇴 회원 삭제 규칙 신설 — 창은 1년 |
| — | 2025-04-25 | `a5b865d7` | 탈퇴 회원 삭제 진입 파일이 현재 경로로 신설 — 최상위가 함수 표현식인 형태가 이때부터다 |
| — | 2025-12-11 | `0e470689` | 청구서 파일 삭제 배치 신설 — 창은 30일 |
| — | 2026-05-18 | `869664be` | 분석결과·원본 데이터의 창을 1년에서 30일로 단축 |
| — | 2026-06-04 | `8e389132` | 남아 있던 1년·90일 경로를 30일로 통일 |

**탈퇴 회원 창만 1년으로 남았다.** 위 두 단축은 분석·원본 축에만 적용됐고 `fc.repository.ts` 의
1년은 그대로다. 이 표는 그 사실을 시점으로 보이지만 *왜* 그 축만 남았는지는 저장소가 말하지
않는다.
