---
template: policy-doc@1
doc_id: 03-data-model
title: 데이터 모델 계약 — 저장 계층이 지는 제약과 그 정책적 의미
config: ../_meta/template-config.yaml
last_verified: 4d798f55
policies:
  - id: data:schema-single-source
    status: active-prod
    evidence: [S-schema, S-contract]
    limitations:
      - 저장 정의는 운영 데이터베이스를 되읽어 만든 생성물이며 이 문서는 그 데이터베이스의 덤프를 확보하지 않았다. 선언과 실물이 어긋나 있어도 이 문서는 그 차이를 보지 못한다
    anchors:
      - insuniverse-server/drizzle/schema.ts:2
      - insuniverse-server/drizzle/schema.ts:3006
      - insuniverse-server/apps/api/src/modules/money/money-wallet.entity.ts:1
      - insuniverse-server/apps/api/src/modules/money/money-wallet.entity.ts:7
  - id: data:surrogate-key
    status: active-prod
    evidence: [S-schema, S-contract]
    anchors:
      - insuniverse-server/drizzle/schema.ts:3007
      - insuniverse-server/drizzle/schema.ts:3017
      - insuniverse-server/drizzle/schema.ts:161
      - insuniverse-server/apps/api/src/modules/money/money-wallet.entity.ts:9
  - id: data:required-columns
    status: active-prod
    evidence: [S-schema, A-comment]
    limitations:
      - 비어 있음이 무엇을 뜻하는지는 칸마다 다르고 저장 정의가 그것을 적지 않는다. 뜻이 인접 설명에 적힌 칸만 이 문서가 판정했다
    anchors:
      - insuniverse-server/drizzle/schema.ts:3010
      - insuniverse-server/drizzle/schema.ts:3012
      - insuniverse-server/drizzle/schema.ts:38
      - insuniverse-server/drizzle/schema.ts:450
  - id: data:column-defaults
    status: active-prod
    evidence: [S-schema, A-comment]
    limitations:
      - 세 채널은 기계 유도이며 채널 밖의 수치 기본값이 정책적이지 않다는 보장은 아니다. 뜻이 적히지 않은 수치 기본값은 판정하지 않고 남겨 둔다
    anchors:
      - insuniverse-server/drizzle/schema.ts:3013
      - insuniverse-server/drizzle/schema.ts:1644
      - insuniverse-server/drizzle/schema.ts:461
      - insuniverse-server/drizzle/schema.ts:432
  - id: data:audit-timestamps
    status: active-prod
    evidence: [S-schema, S-contract]
    limitations:
      - 고친 시각 칸에 값을 넣는 응용 경로는 이 문서가 확인하지 않았다. 저장 계층이 그것을 하지 않는다는 것만 확인했다
    anchors:
      - insuniverse-server/drizzle/schema.ts:3013
      - insuniverse-server/drizzle/schema.ts:1645
      - insuniverse-server/drizzle/schema.ts:150
      - insuniverse-server/apps/api/src/modules/money/money-wallet.entity.ts:27
  - id: data:referential-integrity
    status: active-prod
    evidence: [S-schema, A-comment]
    anchors:
      - insuniverse-server/drizzle/schema.ts:156
      - insuniverse-server/drizzle/schema.ts:160
      - insuniverse-server/drizzle/schema.ts:1631
      - insuniverse-server/drizzle/schema.ts:427
  - id: data:uniqueness
    status: active-prod
    evidence: [S-schema, A-comment]
    anchors:
      - insuniverse-server/drizzle/schema.ts:442
      - insuniverse-server/drizzle/schema.ts:443
      - insuniverse-server/drizzle/schema.ts:421
      - insuniverse-server/drizzle/schema.ts:3018
  - id: data:money-integer
    status: active-prod
    evidence: [S-schema, A-comment]
    anchors:
      - insuniverse-server/drizzle/schema.ts:3010
      - insuniverse-server/drizzle/schema.ts:3011
      - insuniverse-server/drizzle/schema.ts:463
      - insuniverse-server/drizzle/schema.ts:462
  - id: data:delete-marking-not-uniform
    status: active-prod
    evidence: [S-schema, A-comment]
    anchors:
      - insuniverse-server/drizzle/schema.ts:1202
      - insuniverse-server/drizzle/schema.ts:1640
      - insuniverse-server/drizzle/schema.ts:1641
      - insuniverse-server/drizzle/schema.ts:432
      - insuniverse-server/apps/api/src/modules/agency/organization/organization.entity.ts:70
  - id: data:code-column-unconstrained
    status: data-dependent-unknown
    evidence: [S-schema, A-comment]
    unknown_reason: 저장 계층이 허용 낱말 집합을 강제하지 않아 실제로 저장된 값이 그 집합 안에 있는지를 코드에서 판정할 수 없다. 저장된 값을 직접 세지 않고는 목록 밖 값의 유무를 말할 수 없으며 이 문서는 그것을 확보하지 않았다
    anchors:
      - insuniverse-server/drizzle/schema.ts:1634
      - insuniverse-server/drizzle/schema.ts:1963
      - insuniverse-server/drizzle/schema.ts:460
      - insuniverse-server/drizzle/schema.ts:2
  - id: data:staged-only-definitions
    status: staged-only
    evidence: [S-schema, A-release]
    limitations:
      - 미도달 판정의 근거는 각 선언 줄의 최종 변경 커밋 하나다. 그 줄이 뒤에 다른 커밋으로 밀리면 같은 정의가 도달로 뒤집힐 수 있다
    anchors:
      - insuniverse-server/drizzle/schema.ts:1763
      - insuniverse-server/drizzle/schema.ts:3073
      - insuniverse-server/drizzle/schema.ts:3100
      - insuniverse-server/drizzle/schema.ts:405
      - insuniverse-server/drizzle/schema.ts:1616
      - insuniverse-server/drizzle/schema.ts:1956
      - insuniverse-server/drizzle/schema.ts:1975
---

# 데이터 모델 계약 — 저장 계층이 지는 제약과 그 정책적 의미

이 문서는 **저장 계층이 지는 제약**만 다룬다. 어떤 값이 저장될 수 있고, 저장되지 못하면
무엇이 막는가, 그리고 그 막힘이 업무 규칙인가 기술적 장치인가가 여기다. 요청 값이 서버에
받아들여지는 조건은 입력 검증 문서가, 그 결과가 어떤 모양으로 돌아가는지는 응답 계약 문서가
든다.

**표의 목록은 이 문서의 산출물이 아니다.** 제약의 갈래와 그 갈래가 이용자에게 만드는 결과를
쓰고, 어느 표와 어느 칸이 그 갈래에 속하는지는 강제 지점 절의 앵커가 답한다. 개별 낱말
집합과 그 사이의 이동, 금액 상한과 시간 한도의 값 자체도 여기 없다 — 각각 다른 문서의
소유다.

## 정책 (기획)

### data:schema-single-source

저장 계층의 정의는 **한 자리에만 있다.** 어떤 표가 있고 각 칸이 무엇을 받는지는 그 한 자리가
정한다. 그 밖의 곳에 표와 비슷한 이름을 단 선언이 있어도 그것은 저장 정의가 아니라 **응답을
만들 때 쓰는 표현 틀**이다. 이름이 비슷해서 생기는 오해가 실무에서 자주 나온다.

이 구별이 만드는 결과: 표현 틀에 항목을 더해도 저장되는 값은 늘지 않고, 저장 정의를 고치지
않은 채 표현 틀만 고치면 화면에는 빈 자리가 나온다. 반대로 저장 정의를 고쳤는데 표현 틀을
그대로 두면 저장은 되지만 밖으로 나가지 않는다.

또 하나. 그 정의 자체가 운영 데이터베이스를 되읽어 만든 것이다. 되읽은 시점 뒤에
데이터베이스만 고쳐졌다면 이 문서는 그 차이를 보지 못한다. 확인하지 않은 것을 확인했다고
쓰지 않는다.

### data:surrogate-key

표마다 행 하나를 가리키는 열쇠가 있다. 이 저장 계층에서 **열쇠가 없는 표는 없다.** 열쇠는 두
갈래로 선언된다 — 값이 저절로 올라가는 한 칸을 열쇠로 삼는 쪽과, 여러 칸을 묶어 열쇠로 삼는
쪽이다.

이용자에게 만드는 결과는 두 갈래가 다르다. 앞쪽 표는 같은 내용을 거듭 넣어도 서로 다른 행으로
남는다. 뒤쪽 표는 묶인 칸이 모두 같으면 두 번째 입력이 거부된다. 어느 쪽인지는 표마다 다르고
이름만 보고는 알 수 없다.

### data:required-columns

칸마다 **비워 둘 수 있는지 없는지**가 정해져 있다. 비울 수 없는 칸을 비운 채 저장하려 하면 그
저장은 실패한다. 응용이 먼저 걸러 주는 자리가 많지만, 걸러지지 않고 도달하면 저장 계층에서
막힌다.

비울 수 있다는 것이 곧 **선택 항목**이라는 뜻은 아니다. 비어 있음이 아직 값이 없다는 뜻인 칸도
있고, 그 값이 존재하지 않는다는 뜻인 칸도 있으며, 재는 데 실패했다는 뜻인 칸도 있다. 그 뜻은
저장 정의가 아니라 그 옆에 적힌 설명이 진다. 설명이 없는 칸의 뜻은 이 문서가 판정하지 않는다.

지금은 비울 수 있게 두었으나 **나중에 비울 수 없게 바꾸기로 예고된** 칸도 있다. 그 칸은 지금
비워도 저장되지만 예고된 변경 뒤에는 막힌다.

### data:column-defaults

값을 보내지 않았을 때 대신 채워지는 값이 정해진 칸이 있다. **모든 기본값이 정책은 아니다.**
온보딩이 묻는 구별 기준이 여기다. 이 문서는 셋으로 가른다.

첫째, 시각을 넣는 칸에 지금을 채우는 기본값은 **기술적 기본값**이다. 무엇이 언제 만들어졌는지
남기기 위한 장치이며 업무 규칙이 아니다. 담당자가 알아야 할 값이 아니다.

둘째, 숫자가 아닌 값을 채우는 기본값은 **정책**이다. 켜짐과 꺼짐, 쓰임과 안 쓰임, 진행 단계의
시작점처럼 뜻을 가진 낱말이 기본값 자리에 있다는 것은 그 낱말이 업무상 출발 상태라는 뜻이다.

셋째, 숫자를 채우는 기본값 중 **그 숫자의 뜻이 바로 옆에 적혀 있는** 것은 정책이다. 뜻이 적혀
있지 않은 숫자 기본값은 대개 꺼짐이나 없음을 나타내는 자리 채우기이며, 이 문서는 그것을
정책으로 올리지 않는다.

따라서 판정 기준은 이렇게 읽는다 — 기본값이 시각 장치인가, 기본값이 뜻을 가진 낱말인가,
기본값 옆에 그 값의 뜻이 적혀 있는가. 뒤의 둘에 해당하면 그 칸의 기본값은 업무 담당자가
알아야 하는 값이다. 값 자체는 여기 옮기지 않는다.

### data:audit-timestamps

행이 언제 만들어졌는지는 저장 계층이 **저절로** 기록한다. 만든 시각 칸은 값을 보내지 않아도
채워진다.

고친 시각은 다르다. 대부분의 표에서 고친 시각 칸은 **저절로 갱신되지 않는다.** 응용이 값을
넣어 주지 않으면 비어 있거나 예전 값 그대로 남는다. 저절로 갱신되는 표는 참조용 분류표 몇
개뿐이며 업무 데이터를 담는 표에는 그 장치가 없다.

이용자에게 만드는 결과: 목록에서 마지막 수정 시각을 믿고 정렬하면 실제로 고쳐진 순서와 다를
수 있다. 만든 시각에는 그런 위험이 없다.

### data:referential-integrity

표와 표를 잇는 참조는 대부분 **선언되어 있고** 저장 계층이 지킨다. 다만 어떻게 지키는지가 한
가지가 아니다.

가리키는 대상을 지우려 할 때, 어떤 참조는 그 삭제 자체를 막고 어떤 참조는 딸린 행을 함께
지운다. 드물게 가리키는 자리를 비우는 쪽도 있다. 어느 쪽인지에 따라 지웠는데 딸린 것이
남는가와 지웠더니 딸린 것까지 사라지는가가 갈린다.

그리고 **선언되지 않은 참조가 있다.** 자기 자신을 가리키는 계층 구조 하나는 저장 정의가 아니라
그 밖에서 만들어졌다고 옆에 적혀 있다. 그 참조는 정의만 읽어서는 보이지 않는다. 저장 계층이
모든 참조를 지킨다고 읽으면 틀린다.

### data:uniqueness

같은 값을 거듭 넣을 수 없는 자리가 있다. 한 칸만 보는 것도 있고 여러 칸을 묶어 보는 것도
있다. 묶어 보는 쪽은 각 칸이 겹쳐도 되고 묶음이 통째로 겹칠 때만 거부된다.

이용자에게 만드는 결과: 두 번째 입력이 저장 단계에서 거부되며, 그 거부는 화면 검사와 별개로
일어난다. 화면이 막지 못한 중복도 여기서 막힌다.

빠른 조회를 위한 색인은 이 규칙과 다르다. 색인이 걸려 있다고 해서 중복이 막히지는 않는다.
둘은 이름이 비슷하나 색인은 통과 조건이 아니다.

### data:money-integer

금액은 **쪼갤 수 없는 단위의 정수**로 저장된다. 소수 자리를 가진 금액 칸은 없다. 따라서 나누어
떨어지지 않는 계산은 어딘가에서 반드시 버림이나 올림이 일어나며, 그 처리를 정하는 것은 저장
계층이 아니라 응용이다.

소수를 쓰는 칸이 아주 드물게 있으나 **그중 금액인 것은 없다.** 비율인 것도 있고 좌표인 것도
있고 세는 값인 것도 있다. 금액과 이들을 같은 규칙으로 다루면 어긋난다.

### data:delete-marking-not-uniform

지운 것을 실제로 지우지 않고 **표시만 남기는** 자리가 있다. 그런데 그 표시 방식이 **한 가지가
아니다.** 지움 여부를 참거짓으로 적는 표가 있고, 지운 시각을 적는 표가 있고, 쓰임 여부를
낱말로 적는 표가 있다. 감춤 여부와 지움 여부를 따로 두는 표도 있다.

따라서 이 시스템에 지움 표시 규약이 하나 있다는 서술은 성립하지 않는다. 표마다 확인해야 한다.
한 표에서 통한 방식이 다른 표에서 통하지 않는다.

이용자에게 만드는 결과: 지운 뒤에도 데이터는 남아 있으며, 어느 화면에서 사라지고 어느 집계에
남는지가 표마다 다르다. 응답에 나가는 항목 이름이 저장 칸 이름과 다른 자리도 있어 둘을 같은
것으로 놓고 읽으면 어긋난다.

## 강제 지점 (개발)

### data:schema-single-source

- 저장 정의의 유일 소재지: `insuniverse-server/drizzle/schema.ts`. 동결 기준점 전체에서 칸의 필수 여부를 선언하는 호출을 담은 파일이 이 하나다. 임포트 목록은 `schema.ts:2`, 표 선언의 형태는 `schema.ts:3006` 이 보인다.
- 재유도 계수 — 테이블 총수: 155. 문서가 옮겨 적은 값이 아니라 게이트가 동결 기준점의 정의 blob 에서 다시 센 값이며, 게이트 `#8` 이 이 문면과 대조한다. 코드 인덱스 산출물은 이 대조에 쓰지 않는다.
- 표현 틀과의 구별: `insuniverse-server/apps/api/src/modules/money/money-wallet.entity.ts:1` 이 직렬화 라이브러리를 들여오고 `money-wallet.entity.ts:7` 이 응답 표현 클래스를 연다. 이름에 저장 개체를 뜻하는 접미어가 붙지만 저장 정의가 아니다. 같은 접미어를 가진 파일이 동결 기준점에 다수 있으며 전부 같은 성격이다.
- 한계: 정의 파일은 데이터베이스를 되읽어 만든 생성물이다. 덤프를 확보하지 않았으므로 선언과 실물의 대조는 하지 않았다.

### data:surrogate-key

- 한 칸 열쇠: `schema.ts:3007` — 값이 저절로 올라가는 칸에 열쇠 표시가 붙는다. 같은 표의 표 수준 선언은 `schema.ts:3017`.
- 묶음 열쇠: `schema.ts:161` — 네 칸을 묶어 열쇠로 삼는다.
- 재유도 계수 — PK 미보유 테이블: 0. **이 수는 코드 인덱스가 아니라 정의 소스에서 유도한다.** 인덱스는 표 수준 선언만 세어 다른 답을 내며, 그 값을 실으면 열쇠 없는 표가 열 개 있다는 거짓 정책이 된다. 게이트 `#8` 이 칸 수준과 표 수준의 합집합으로 다시 세고 인덱스를 읽지 않는다.
- 응답 표현에서의 대응: `money-wallet.entity.ts:9`.

### data:required-columns

- 비울 수 없는 칸: `schema.ts:3010`. 비울 수 있는 칸: `schema.ts:3012`. 같은 표 안에 두 형태가 나란히 있다.
- 비어 있음의 뜻은 정의가 아니라 인접 설명이 진다: `schema.ts:38` 이 재지 못한 경우를 비어 있음으로 쓴다고 적는다.
- 예고된 변경: `schema.ts:450` 이 그 칸을 나중에 비울 수 없게 바꾸겠다고 적는다. 지금은 비울 수 있다.

### data:column-defaults

- 기술적 기본값(시각): `schema.ts:3013`.
- 낱말 기본값: `schema.ts:461` 이 계약 상태의 출발점, `schema.ts:432` 가 쓰임 여부의 출발점이다.
- 뜻이 적힌 수치 기본값: `schema.ts:1644` — 같은 줄 끝에 그 값이 무엇을 뜻하는지가 적혀 있다.
- 세 채널은 전부 기계 유도다. 낱말 기본값은 기본값의 형태로, 상태 칸의 기본값은 코드 인덱스의 상태 칸 분류로, 뜻이 적힌 수치 기본값은 인접 설명의 유무로 가른다. 손으로 고른 목록이 아니다.
- 값 자체는 여기 옮기지 않는다 — 한도 성격을 갖는 기본값이 실재하나 그 수치의 소유는 한도 축 문서다.

### data:audit-timestamps

- 만든 시각 자동 기록: `schema.ts:3013`.
- 고친 시각은 자동이 아니다: `schema.ts:1645` — 기본값도 자동 갱신도 붙지 않는다.
- 자동 갱신이 걸린 자리: `schema.ts:150`. 동결 기준점에서 이 형태는 참조용 분류표에만 있다.
- 응답 표현에서의 대응: `money-wallet.entity.ts:27`.

### data:referential-integrity

- 표 수준 선언: `schema.ts:156` 이 열고 `schema.ts:160` 이 삭제·갱신 시 동작을 붙인다.
- 칸 수준 선언: `schema.ts:1631` — 삭제를 막는 쪽이다.
- 재유도 계수 — 선언된 참조 제약: 101. 표 수준과 칸 수준을 합한 수이며 게이트 `#8` 이 소스에서 다시 센다. 표 수준 선언은 그중 일부이고 전부 질병 분류 계층에 몰려 있다.
- 선언되지 않은 참조: `schema.ts:427` 이 자기 참조 계층을 정의 밖에서 만들었다고 적는다. 정의만 읽으면 보이지 않는다.

### data:uniqueness

- 묶음 중복 금지: `schema.ts:442` · `schema.ts:443`. 둘 다 두 칸을 묶어 본다.
- 뜻을 적은 설명: `schema.ts:421`.
- 조회 색인과의 구별: `schema.ts:3018` 은 색인이며 중복을 막지 않는다.
- 재유도 계수 — 선언된 유일 제약: 40. 조회 색인은 이 수에 들지 않는다.

### data:money-integer

- 금액 칸: `schema.ts:3010` · `schema.ts:3011`. 둘 다 정수 형이다.
- 소수 칸의 실제 용도: `schema.ts:463` 은 비율이며 그 뜻은 바로 위 `schema.ts:462` 에 적혀 있다. `schema.ts:606` 은 알림톡 표의 세는 값이고 금액이 아니다. 나머지 둘은 청구서 좌표다.
- 재유도 계수 — 소수 칸: 4. 게이트 `#8` 이 동결 기준점의 정의 blob 에서 소수 형 컬럼을 다시 센다. 문서가 훑어 적은 목록이 아니라 재유도된 수이며, 그중 금액 칸은 없다.

### data:delete-marking-not-uniform

- 참거짓 표시: `schema.ts:1202` · `schema.ts:1640`.
- 시각 표시: `schema.ts:1641`.
- 낱말 표시: `schema.ts:432`.
- 감춤과 지움을 따로 두는 자리: `schema.ts:1639` 와 `schema.ts:1640` 이 한 표 안에 함께 있다.
- 저장 칸 이름과 응답 항목 이름의 차이: `insuniverse-server/apps/api/src/modules/agency/organization/organization.entity.ts:70` 이 그 매핑을 적는다.

### data:code-column-unconstrained

- 낱말을 담는 칸: `schema.ts:1634` · `schema.ts:1963`. 둘 다 짧은 글자 칸이며 허용 집합 강제가 없다.
- 허용 낱말이 적힌 자리: `schema.ts:460` — 저장 정의가 아니라 인접 설명이다.
- 강제 수단의 부재: `schema.ts:2` 의 임포트 목록에 값 집합을 강제하는 칸 형태가 없다. 동결 기준점의 스키마 제약 인덱스에도 그 형태는 없다.
- 판정 불가 사유: 저장된 값을 직접 세지 않고는 목록 밖 값의 유무를 말할 수 없다. 그 사유는 프론트매터의 미판정 사유가 그대로 든다.

### data:staged-only-definitions

- 표째 미도달: `schema.ts:1763` · `schema.ts:3073` · `schema.ts:3100`.
- 도달한 표 안의 미도달 칸: `schema.ts:405` · `schema.ts:1616`.
- 도달한 표 안의 미도달 중복 금지: `schema.ts:1956` · `schema.ts:1975`.
- 재유도 계수 — 미도달 테이블: 3 · 미도달 컬럼: 2. 두 수는 각 선언 줄의 최종 변경 커밋을 동결 기준점에서 다시 뽑아 릴리스 산출물의 미도달 집합과 대조해 얻는다. 중복 금지 규칙 쪽은 계수를 결속하지 않고 앵커로만 든다.
- 한계: 미도달 판정은 선언 줄 하나의 최종 변경 커밋에 걸린다. 그 줄이 뒤에 다른 커밋으로 밀리면 같은 정의가 도달로 뒤집힐 수 있다.

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

| 정책 id | 관리자 | 영업대리점 | 기업담당자 | 일반회원 | 기업회원 | server | site | admin |
|---|---|---|---|---|---|---|---|---|
| data:schema-single-source | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| data:surrogate-key | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| data:required-columns | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| data:column-defaults | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| data:audit-timestamps | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| data:referential-integrity | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| data:uniqueness | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| data:money-integer | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| data:delete-marking-not-uniform | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| data:code-column-unconstrained | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| data:staged-only-definitions | 미적용 | 미적용 | 미적용 | 미적용 | 미적용 | 강제 지점 | — | — |

이 문서의 정책은 전부 **저장 계층**이 진다. 저장 계층은 요청을 보낸 주체를 구별하지 않으므로
actor 축은 전건 `적용` 이다 — 어느 actor 의 쓰기든 이 표들에 닿으면 같은 제약을 지난다. 어느
actor 가 어느 표에 닿는지는 이 문서가 판정하지 않으며 도메인 문서의 소유다.

마지막 행만 다르다. 그 정의는 동결 기준점에서 운영에 도달하지 않았으므로 지금은 어느 actor 의
요청도 그 제약을 지나지 않는다. `강제 지점` 은 정의가 이미 그 저장소에 있다는 뜻이고
`—` 는 그 서비스가 아직 이 정의의 경로에 등장하지 않는다는 뜻이다.

## 예외·폴백

| 정책 id | 예외 조건 | 폴백 동작 | 확인 근거 |
|---|---|---|---|
| data:schema-single-source | 정의 파일과 실제 데이터베이스가 어긋난 경우 | 확인하지 않았다 — 덤프를 확보하지 않아 이 문서는 그 상태를 판정하지 못한다 | `schema.ts:2` |
| data:surrogate-key | 없음 — 동결 기준점의 표 전건이 칸 수준 또는 표 수준 열쇠를 갖는다 | — | `schema.ts:3007` · `schema.ts:161` |
| data:required-columns | 칸에 필수 표시가 없음 | 비운 채로 저장된다. 그 비어 있음의 뜻은 인접 설명이 지며 설명이 없으면 판정하지 않는다 | `schema.ts:3012` · `schema.ts:38` |
| data:required-columns | 지금은 선택이나 필수로 바꾸기로 예고된 칸 | 지금은 저장되고 예고된 변경 뒤에는 막힌다 | `schema.ts:450` |
| data:column-defaults | 칸에 기본값이 없음 | 값을 보내지 않으면 비어 있거나 저장이 실패한다. 어느 쪽인지는 필수 여부가 정한다 | `schema.ts:1645` |
| data:column-defaults | 수치 기본값에 뜻이 적혀 있지 않음 | 정책으로 올리지 않고 자리 채우기로 둔다 | `schema.ts:1644` |
| data:audit-timestamps | 고친 시각 칸에 자동 갱신이 붙지 않은 표 | 응용이 값을 넣지 않으면 갱신되지 않는다 | `schema.ts:1645` · `schema.ts:150` |
| data:referential-integrity | 참조가 저장 정의 밖에서 만들어진 경우 | 정의만 읽어서는 보이지 않는다. 옆에 적힌 설명이 유일한 단서다 | `schema.ts:427` |
| data:referential-integrity | 가리키는 대상을 지우려 할 때 | 삭제를 막거나, 딸린 행을 함께 지우거나, 가리키는 자리를 비운다. 셋 중 어느 것인지는 선언이 정한다 | `schema.ts:160` · `schema.ts:1631` |
| data:uniqueness | 색인만 걸리고 중복 금지가 없는 칸 | 중복이 저장된다 | `schema.ts:3018` |
| data:money-integer | 나누어 떨어지지 않는 계산 | 저장 계층이 정하지 않는다. 버림·올림의 처리는 응용의 소유다 | `schema.ts:3010` |
| data:money-integer | 소수 칸 | 금액이 아니다 — 비율·좌표·세는 값이다 | `schema.ts:463` · `schema.ts:606` |
| data:delete-marking-not-uniform | 표마다 표시 방식이 다름 | 한 표의 방식을 다른 표에 적용할 수 없다. 표마다 확인한다 | `schema.ts:1202` · `schema.ts:1641` · `schema.ts:432` |
| data:code-column-unconstrained | 목록 밖 낱말이 저장된 경우 | 저장 계층이 막지 않는다. 화면이 그 값을 어떻게 다룰지는 정해져 있지 않다 | `schema.ts:1634` · `schema.ts:460` |
| data:staged-only-definitions | 도달한 표 안에 미도달 칸이나 미도달 중복 금지가 붙은 경우 | 표가 쓰이고 있어 정의를 읽으면 살아 있는 것처럼 보인다. 도달 판정은 선언 줄의 최종 변경 커밋이 진다 | `schema.ts:405` · `schema.ts:1956` |

`없음` 한 건은 확인해서 없는 것이다 — 동결 기준점의 표 전수를 훑어 칸 수준 열쇠와 표 수준
열쇠의 합집합을 취했고 둘 중 하나도 갖지 않는 표를 찾지 못했다. 찾지 못한 것과 존재하지
않는 것의 차이는 이 문장이 진다. 첫 행의 `확인하지 않았다` 는 그와 다르다 — 볼 수단을
확보하지 못했다는 뜻이다.

## 부록

이 절의 정책 둘은 서로 다른 이유로 본문에서 격리된다. 하나는 **저장된 값이 무엇인지 코드에서
판정할 수 없어서**이고, 다른 하나는 **정의는 있으나 아직 운영에 도달하지 않아서**다. 본문에
두면 독자가 지금 확정된 사실로 읽는다. 판정이 서거나 도달이 확인되면 표기가 바뀌고 배치도
함께 본문으로 옮긴다.

### data:code-column-unconstrained

정해진 낱말 중 하나를 담기로 한 칸이 여럿 있다. 그런데 **저장 계층은 그 낱말 목록을 강제하지
않는다.** 목록을 아는 것은 응용뿐이고, 저장 계층 입장에서 그 칸은 그저 짧은 글자나 작은 수를
받는 칸이다.

따라서 목록 밖의 값이 들어간 행이 실제로 있는지 없는지는 **정의만 읽어서는 알 수 없다.**
저장된 값을 직접 세어야 알 수 있고 이 문서는 그것을 확보하지 않았다. 없다고 단정하지도,
있다고 단정하지도 않는다.

이용자에게 만드는 결과: 예전에 쓰이다 사라진 낱말이 남아 있을 수 있고, 화면이 그것을 어떻게
보여줄지는 정해져 있지 않다.

### data:staged-only-definitions

정의는 이미 들어와 있으나 **아직 운영에 도달하지 않은** 자리가 있다. 세 갈래다 — 표째로
도달하지 않은 것, 도달한 표 안에 도달하지 않은 칸이 붙은 것, 도달한 표에 도달하지 않은 중복
금지 규칙이 붙은 것이다.

가운데와 마지막이 특히 헷갈린다. 표는 이미 쓰이고 있으므로 정의를 읽으면 그 칸과 규칙도 살아
있는 것처럼 보인다. 그러나 그 줄을 들여온 변경이 운영에 도달한 집합 밖에 있다.

이용자에게 만드는 결과: 이 자리에 기대어 만든 기능은 지금 운영에서 동작하지 않는다. 도달
여부가 바뀌면 이 절의 내용은 본문으로 옮겨간다.

## 연혁

릴리스 태그가 존재하지 않는다. 릴리스 열은 `—` 로 두고 커밋으로 추적한다.

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2025-07-15 | `630a9b1e` | 강의 표가 실제 삭제 대신 지움 표시 칸을 쓰기 시작했다 — 표시 방식이 갈라지는 출발점 |
| — | 2025-11-04 | `407c1914` | 질병 분류 계층에 표 수준 참조 제약과 고친 시각 자동 갱신이 함께 들어왔다 |
| — | 2026-01-12 | `46efac63` | 머니 이력 표가 지금 형태로 정리됐다 — 금액과 잔액이 정수 칸이다 |
| — | 2026-05-20 | `8be772db` | 기업조직 도입 — 조직 이름과 코드에 묶음 중복 금지, 계약 상태에 낱말 기본값, 수수료율에 소수 칸이 함께 들어왔다 |
| — | 2026-06-11 | `884596e9` | 수집 타임아웃 칸에 수치 기본값과 그 값의 뜻을 적은 설명이 함께 붙었다 |
| — | 2026-07-01 | `f0ce46c8` | 조직 계층의 자기 참조가 저장 정의 밖에서 만들어졌다고 명시됐다 |
| — | 2026-07-16 | `89bc923a` | 주문에 과금방식 스냅샷 칸이 추가됐다 (운영 미도달) |
| — | 2026-07-16 | `c577fd29` | 약관 표 둘에 중복 금지 규칙이 추가됐다 (운영 미도달) |
| — | 2026-07-20 | `8507677b` | 자동차 수집 일회 동의 요청 표가 추가됐다 (운영 미도달) |
| — | 2026-07-24 | `54473012` | 기업에 분석 노출 등급 칸이 추가됐다 (운영 미도달) |
| — | 2026-08-05 | `4d798f55` | 손해사정 분석 표 둘이 추가됐다 (운영 미도달) |
