---
template: policy-doc@1
doc_id: 05-external-client-api
title: 대외 계약 API — 고객사 표면의 경계와 실패 어휘 층 분리
config: ../_meta/template-config.yaml
last_verified: 4d798f55
policies:
  - id: client:surface-entry-points
    status: active-prod
    evidence: [S-guard, S-route]
    limitations:
      - 라우트가 없는 등록 컨트롤러를 사장으로 볼지 준비 중으로 볼지는 이 문서가 판정하지 않는다. 그 판정은 연혁·사장 축 문서의 소유다
    anchors:
      - insuniverse-server/apps/api/src/modules/client/client/client.controller.ts:71
      - insuniverse-server/apps/api/src/modules/client/client/client.controller.ts:72
      - insuniverse-server/apps/api/src/modules/treat/coocon-bypass.controller.ts:25
      - insuniverse-server/apps/api/src/modules/treat/coocon-bypass.controller.ts:26
      - insuniverse-server/apps/api/src/modules/client/client/client.module.ts:23
      - insuniverse-server/apps/api/src/modules/client/analyze/client-analyze.module.ts:25
      - insuniverse-server/apps/api/src/modules/client/settlement/settlement.module.ts:11
      - insuniverse-server/apps/api/src/modules/client/analyze/client-analyze.controller.ts:8
      - insuniverse-server/apps/api/src/modules/client/settlement/settlement.controller.ts:8
      - insuniverse-server/apps/api/src/modules/client/client/client-admin.controller.ts:27
  - id: client:credential-boundary
    status: active-prod
    evidence: [S-guard, S-schema]
    anchors:
      - insuniverse-server/apps/api/src/modules/auth/client/auth.guard.ts:34
      - insuniverse-server/apps/api/src/modules/auth/client/auth.guard.ts:54
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:133
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:134
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:135
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:136
  - id: client:error-table-is-separate
    status: active-prod
    evidence: [S-guard, S-contract]
    limitations:
      - 어느 코드가 실제로 얼마나 나가는지는 운영 로그에만 있다. 이 문서는 선언 자리만 읽었다
    anchors:
      - insuniverse-server/apps/api/src/constants/client.constant.ts:12
      - insuniverse-server/apps/api/src/constants/client.constant.ts:29
      - insuniverse-server/apps/api/src/constants/api.constant.ts:16
      - insuniverse-server/apps/api/src/constants/admin.constant.ts:62
      - insuniverse-server/apps/api/src/interceptors/client-response.interceptor.ts:25
      - insuniverse-server/apps/api/src/fliters/client-exception.filter.ts:35
  - id: client:contract-option-per-route
    status: active-prod
    evidence: [S-contract, S-route]
    limitations:
      - 어느 고객사 계약에 어느 옵션이 들어 있는지는 계약 데이터에만 있다. 이 문서는 옵션 어휘와 요구 자리만 읽었다
    anchors:
      - insuniverse-server/apps/api/src/modules/client/client/client.dto.ts:13
      - insuniverse-server/apps/api/src/modules/client/client/client.controller.ts:111
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:176
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:177
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:172
  - id: client:monthly-quota-by-person
    status: active-prod
    evidence: [S-schema, A-comment]
    limitations:
      - 상한 스위치가 켜진 계약이 실재하는지는 운영 데이터에만 있다. 그 판정은 부록 정책이 든다
    anchors:
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:77
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:83
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:146
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:158
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:159
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:161
  - id: client:synchronous-failure-split
    status: active-prod
    evidence: [S-guard, A-comment]
    limitations:
      - 실패 단계에서 남는 낱말이 무엇인지는 상태 전이 문서의 소유다. 이 문서는 기록과 거절의 순서만 든다
    anchors:
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:73
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:84
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:106
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:115
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:129
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:138
  - id: client:entry-points-differ-below-prefix
    status: active-prod
    evidence: [S-guard, A-comment]
    limitations:
      - 우회 진입점이 넘기는 벤더 봉투의 해석 규약은 벤더 축 문서의 소유다. 이 문서는 겉모양이 갈린다는 사실만 든다
    anchors:
      - insuniverse-server/apps/api/src/modules/client/client/client.controller.ts:69
      - insuniverse-server/apps/api/src/modules/client/client/client.controller.ts:70
      - insuniverse-server/apps/api/src/modules/treat/coocon-bypass.controller.ts:55
      - insuniverse-server/apps/api/src/modules/treat/coocon-bypass.controller.ts:57
  - id: client:car-window-cutoff
    status: staged-only
    evidence: [S-constant, B-commit]
    anchors:
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:45
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:436
  - id: client:quota-values-are-contract-data
    status: data-dependent-unknown
    evidence: [S-schema, A-comment]
    unknown_reason: 상한 스위치가 켜져 있는지와 상한이 얼마인지는 계약 행에만 있으며 이 조사는 그 데이터를 확보하지 않았다. 코드는 스위치를 읽고 값을 비교할 뿐 값을 정하지 않는다
    anchors:
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:126
      - insuniverse-server/apps/api/src/modules/client/contract/contract.repository.ts:127
      - insuniverse-server/apps/api/src/modules/client/client/client.service.ts:77
---

# 대외 계약 API — 고객사 표면의 경계와 실패 어휘 층 분리

이 문서는 **고객사가 계약으로 부르는 표면**만 다룬다. 어디까지가 그 표면인가, 그 표면의 실패
어휘가 내부 어휘와 어떻게 갈리는가, 계약이 라우트에 어떻게 결속되는가 세 축이다.

읽는 사람이 여기서 얻어야 하는 것은 **이름이 표면을 정하지 않는다**는 사실이다. 고객사를
가리키는 이름이 붙은 자리 대부분은 관리자 화면이 부르는 내부 표면이고, 대외 표면은 그중 극히
일부이며 그 디렉터리 밖에도 하나 있다. 표면을 정하는 것은 이름도 디렉터리도 아니라 고객사
자격 증명을 확인하는 관문이다.

여기 없는 것: 응답 겉모양 규칙 자체, 입력 검증 규칙과 그 실패 코드, 내부 오류 코드의 뜻,
상태 낱말의 값과 전이, 금액·시간·빈도의 한도값, 벤더 응답 해석과 대기 한도. 각각 다른 문서의
소유이며 이 문서는 그 자리를 가리키기만 한다.

## 정책 (기획)

### client:surface-entry-points

대외 계약 표면은 디렉터리로 정해지지 않는다. **고객사 계정 코드와 비밀번호 쌍을 확인하는
관문을 단 진입점만 대외 표면이다.** 그 관문을 달지 않은 진입점은 이름이 고객사를 가리켜도
내부 표면이며, 관리자 화면이나 관리자 조회 토큰으로 들어온다.

이 구별이 만드는 결과: 고객사 이름이 붙은 하위 모듈을 모두 대외 API 로 읽으면 틀린다. 대다수는
관리자 표면이고, 그중 둘은 모듈에 **등록은 돼 있으나 부를 수 있는 자리가 하나도 없다** — 로드는
되고 라우트만 없다. 그 둘의 관문도 대외 자격 증명이 아니라 내부 세션 관문이다.

라우트 없는 등록 자리를 사장으로 볼지 준비 중으로 볼지 이 문서는 판정하지 않는다. 관측만
적고 처분은 연혁·사장 축 문서로 넘긴다.

### client:credential-boundary

자격 증명이 맞다는 것과 계약이 유효하다는 것은 **다르다.** 관문은 계정 쌍이 등록돼 있는지만
본다. 그다음 업무 처리는 **같은 쌍을 다시 들고** 계약 기간 안에 있는 계약 행을 찾는다. 관문을
통과했더라도 오늘이 계약 기간 밖이면 계약을 찾지 못하고 거절된다.

이 구별이 만드는 결과: 고객사는 자격 증명 실패와 계약 만료를 서로 다른 실패로 받는다. 앞엣것은
관문에서, 뒤엣것은 업무 처리에서 나가며 두 자리가 서로를 대신하지 않는다. 관문 통과를 계약
유효로 읽으면 만료된 고객사가 통과한 것으로 오해하게 된다.

### client:error-table-is-separate

대외 실패 어휘는 내부 실패 어휘와 **다른 표**에 산다. 두 표는 응답에서 코드와 문구를 담는
**칸 이름부터 다르다.** 내부 표는 둘이고 대외 표는 하나다. 대외 표 안에서는 코드값이 항목마다
유일하지만, **내부 두 표에는 같은 코드값을 서로 다른 이름으로 든 자리가 각각 남아 있다.**

표를 건너면 값의 뜻이 이어지지 않는다. **같은 코드값이 대외 표와 내부 표에 함께 있으면서 뜻이
서로 다른 자리가 있고, 반대로 상수 이름이 같은데 값이 다른 자리도 있다.** 값이 표 안에서도
갈리므로 조회 키는 값 하나가 아니라 **어느 표의 어느 이름인가** 하는 쌍이다. 값만 들고 표를
건너 뜻을 옮기면 틀리고, 값만 들고 한 표 안에서 이름을 되찾으려 해도 내부 표에서는 되찾히지
않는다.

이것이 잘못이라고 판정하지 않는다. 표가 나뉘어 있다는 것과 표 안에서도 값이 늘 유일하지는
않다는 것이 관측이며, 파일을 건너선 중복은 어느 문서도 열거하지 않는다고 오류 코드 문서가
스스로 적어 두었다. 이 문서가 채우는 자리는 그 문장이 비워 둔 자리다.

### client:contract-option-per-route

결과 조회는 **라우트마다 계약 옵션 하나를 못박아** 요청한다. 고객사가 요청에 옵션을 실어 보내는
것이 아니라 부른 자리가 옵션을 정하며, 그 옵션이 그 고객사의 계약에 들어 있지 않으면 계약 옵션
없음으로 거절된다. 옵션을 고정하지 않는 자리는 진단코드 검색과 분석 요청 둘뿐이다.

옵션 어휘에는 **어느 라우트도 요구하지 않는 원소가 남아 있다.** 계약에 그 옵션을 넣을 수는
있으나 그것을 요구하는 조회 자리가 없어, 넣어도 아무 조회를 열지 않는다. 사실로만 적고 잘못인지
판정하지 않는다.

### client:monthly-quota-by-person

월간 상한은 고객사 단위가 아니라 **사람 단위**다. 계약 행의 상한 스위치가 켜져 있으면, 같은
계약에서 같은 개인 식별자로 **완료된** 요청을 달력 월 경계로 세어 상한과 비교한다. 상한에 닿으면
그 사람의 새 요청이 거절되고 같은 계약의 다른 사람은 영향을 받지 않는다.

경계는 달력 월이며 월이 바뀌면 셈이 다시 시작한다. 완료되지 않은 요청은 세지 않는다 — 실패로
끝난 요청은 상한을 소모하지 않는다.

### client:synchronous-failure-split

분석 요청은 **동기**다. 요청 하나가 계약 확인·상한 확인·원본 저장·분석을 차례로 지나고 응답이
돌아올 때 그 처리가 이미 끝나 있다. 각 단계의 실패는 **서로 다른 갈래의 코드**로 나간다. 계약을
찾지 못하면 계약 갈래, 상한을 넘으면 상한 코드, 원본 저장이 실패하면 주문 갈래, 분석이 실패하면
분석 갈래다.

순서가 중요하다. 저장 이후 단계에서 실패하면 **결과가 먼저 기록되고 그다음 거절된다.** 고객사는
실패 응답을 받지만 그 요청의 흔적은 남아 있고, 나중에 결과 조회로 그 요청을 다시 가리킬 수
있다. 어떤 낱말로 남는지는 상태 전이 문서의 소유다.

### client:entry-points-differ-below-prefix

같은 접두 아래 있다고 같은 계약이 아니다. 대외 관문을 다는 진입점 둘 중 **하나만** 대외 성공
겉모양과 실패 겉모양을 함께 단다. 다른 하나는 수집 벤더의 봉투를 그대로 넘기는 우회 진입점이며,
동시 처리 한도를 넘겼을 때 **대외 표의 칸 이름도 아니고 대외 표의 코드 모양도 아닌 형태로**
던진다.

이 비대칭이 만드는 결과: 접두만 보고 응답 모양을 가정하면 틀린다. 같은 자격 증명으로 두 진입점을
쓰는 고객사는 성공과 실패의 겉모양이 자리마다 다르다는 것을 알아야 한다. 우회 진입점이 넘기는
벤더 봉투의 해석 규약과 대기 한도는 벤더 축 문서의 소유이며 여기서는 겉모양이 갈린다는 사실까지만
적는다.

## 강제 지점 (개발)

### client:surface-entry-points

관문 데코레이터가 표면의 분모다 — `client.controller.ts:71` 이 대외 관문을 달고 `:72` 가 접두를
정한다. 같은 관문을 다는 진입점이 그 디렉터리 밖에 하나 더 있다: `coocon-bypass.controller.ts:25`
와 `:26`. 이 둘 밖의 고객사 이름 컨트롤러는 관리자 관문이거나 내부 세션 관문이며,
`client-admin.controller.ts:27` 이 그 관리자 관문의 형태다.

등록과 라우트는 별개다. `client.module.ts:23` · `client-analyze.module.ts:25` ·
`settlement.module.ts:11` 이 컨트롤러를 등록하고, 그중 `client-analyze.controller.ts:8` 과
`settlement.controller.ts:8` 은 접두만 있고 HTTP 데코레이터가 없다.

- 결속 집합 — entry_prefixes: `clients` · `clients/coocon`
- 결속 집합 — client_routes: `clients/analyze` · `clients/analyze/car` · `clients/analyze/health/3month` · `clients/analyze/health/disease` · `clients/analyze/health/dosing30d` · `clients/analyze/health/examination` · `clients/analyze/health/hospitalization` · `clients/analyze/health/medication` · `clients/analyze/health/treatment7d` · `clients/analyze/hidden/dental` · `clients/analyze/hidden/diagnosis` · `clients/analyze/hidden/etc` · `clients/analyze/hidden/surgery` · `clients/analyze/log/diagnosis` · `clients/analyze/log/examination` · `clients/analyze/log/medication` · `clients/analyze/log/treatment` · `clients/analyze/notice` · `clients/analyze/sick/3month` · `clients/analyze/sick/disease` · `clients/analyze/sick/hospitalization2y` · `clients/analyze/sick/hospitalization5y` · `clients/coocon/scrap` · `clients/search/:code`
- 결속 집합 — module_controllers: `AdminClientAnalyzeController` · `AdminClientContractController` · `AdminClientController` · `AdminClientOrderDetailController` · `AdminViewClientAnalyzeController` · `ClientAnalyzeController` · `ClientController` · `ClientSettlementAdminController` · `ClientSettlementController`
- 층 분리 재유도 — mounted-zero-route: `ClientAnalyzeController` · `ClientSettlementController`

### client:credential-boundary

관문은 `auth.guard.ts:54` 에서 계정 코드와 비밀번호 쌍으로 대외 계정 행을 찾고, `:34` 에서 그
쌍을 요청에 실어 다음 계층으로 넘긴다. 계약 조회는 `contract.repository.ts:133`·`:134` 에서
**같은 쌍을 조인 조건에 다시 넣고** `:135`·`:136` 에서 계약 기간 창을 건다. 관문과 계약 조회가
서로 다른 두 테이블을 보며, 뒤엣것이 앞엣것을 다시 확인한다.

### client:error-table-is-separate

대외 표는 `client.constant.ts` 하나이고 성공 항목이 `:12`, 첫 실패 항목이 `:29` 다. 내부 표는
`api.constant.ts` 와 `admin.constant.ts` 둘이며 각각 `api.constant.ts:16` 과
`admin.constant.ts:62` 가 대외 표 `:29` 와 **같은 코드값을 서로 다른 뜻으로** 든다. 대외 표만
칸 이름이 다르고 내부 두 표는 같다.

겉모양을 다는 자리는 `client-response.interceptor.ts:25` 가 성공, `client-exception.filter.ts:35`
가 잡히지 않은 실패다. 둘 다 대외 표의 항목을 펼쳐 응답에 싣는다.

- 결속 집합 — client_codes: `E0001` · `E0002` · `E0003` · `E0004` · `E0101` · `E0102` · `E0103` · `E0201` · `E0202` · `E0301` · `E9999` · `S0000`
- 층 분리 재유도 — value-across-tables: `E0001` · `E0002` · `E0003` · `E0004` · `E0101` · `E0102` · `E0201` · `E0202` · `E0301`
- 층 분리 재유도 — name-across-tables: `ANALYSIS_FAILED` · `NOT_FOUND_ORDER`

`value-across-tables` 는 대외 표의 코드값 중 내부 두 표 어느 한쪽에도 있는 값이고,
`name-across-tables` 는 상수 이름이 대외 표와 내부 표에 함께 있으면서 **값이 다른** 것이다. 두
집합은 세 표를 각각 읽어 재구성한다 — 어느 목록도 인용하지 않는다.

### client:contract-option-per-route

옵션 어휘는 `client.dto.ts:13` 이 선언한다. 결속은 라우트 안에서 상수 대입으로 일어나며
`client.controller.ts:111` 이 그 형태다. 계약 보유 확인은 `contract.repository.ts:176`·`:177` 이
계약 코드와 옵션 값 쌍으로 매핑 행을 찾고, 없으면 `client.service.ts:172` 가 계약 옵션 갈래로
거절한다.

- 결속 집합 — option_codes: `CCT001` · `CCT002` · `CCT003` · `CCT004` · `CCT005` · `CCT006` · `CCT007` · `CCT008` · `CCT009` · `CCT010` · `CCT011` · `CCT012` · `CCT013` · `CCT014` · `CCT015` · `CCT016` · `CCT017` · `CCT018` · `CCT019` · `CCT020` · `CCT021` · `CCT022` · `CCT023` · `CCT024`
- 결속 집합 — route_option_map: `clients/analyze/car=CCT018` · `clients/analyze/health/3month=CCT005` · `clients/analyze/health/disease=CCT009` · `clients/analyze/health/dosing30d=CCT024` · `clients/analyze/health/examination=CCT006` · `clients/analyze/health/hospitalization=CCT008` · `clients/analyze/health/medication=CCT007` · `clients/analyze/health/treatment7d=CCT023` · `clients/analyze/hidden/dental=CCT003` · `clients/analyze/hidden/diagnosis=CCT001` · `clients/analyze/hidden/etc=CCT004` · `clients/analyze/hidden/surgery=CCT002` · `clients/analyze/log/diagnosis=CCT020` · `clients/analyze/log/examination=CCT022` · `clients/analyze/log/medication=CCT021` · `clients/analyze/log/treatment=CCT019` · `clients/analyze/notice=CCT014` · `clients/analyze/sick/3month=CCT010` · `clients/analyze/sick/disease=CCT013` · `clients/analyze/sick/hospitalization2y=CCT011` · `clients/analyze/sick/hospitalization5y=CCT012`
- 층 분리 재유도 — option-declared-unrouted: `CCT015` · `CCT016` · `CCT017`

`option-declared-unrouted` 는 선언 집합에서 결속 집합을 뺀 차집합이다. 두 집합을 각각 읽어
만들며, 차집합이 비면 이 절의 주장이 사실이 아니게 된 것이므로 그 자리에서 검사가 뒤집힌다.

### client:monthly-quota-by-person

스위치 판정은 `client.service.ts:77`, 상한 비교는 `:83` 이다. 세는 자리는
`contract.repository.ts:158` 이 계약을, `:159` 가 개인 식별자를 조건으로 걸고 `:146` 과 `:161` 이
달력 월 경계를 만든다. 계약이 아니라 **계약 × 사람** 이 셈의 단위이며, 세는 대상은 완료된
요청뿐이다.

### client:synchronous-failure-split

한 요청 안에서 실패 자리가 넷이다. `client.service.ts:73` 이 계약 갈래, `:84` 가 상한, `:115` 가
주문 갈래, `:138` 이 분석 갈래로 던진다. 뒤의 둘은 던지기 **전에** `:106` 과 `:129` 에서 주문상세
결과 칸을 먼저 갱신한다 — 기록이 거절보다 앞선다.

### client:entry-points-differ-below-prefix

겉모양을 다는 것은 `client.controller.ts:69`(성공)와 `:70`(실패) 둘이며 이 데코레이터가 우회
진입점에는 없다. 우회 진입점은 `coocon-bypass.controller.ts:55` 에서 예외를 만들고 `:57` 이 그
칸 이름을 정하는데, 대외 표의 칸 이름이 아니고 값도 대외 표의 코드 모양이 아니다.

### client:car-window-cutoff

시점 컷오프 상수는 `client.service.ts:45` 가 선언하고 `:436` 이 자동차 결과 조회에서 그것을
비교한다. 비교가 참이면 조회가 빈 목록으로 조기 반환되며 예외를 던지지 않는다. 두 행 모두
운영에 닿지 않은 커밋이 만들었다.

### client:quota-values-are-contract-data

상한 스위치와 상한 값은 계약 조회가 `contract.repository.ts:126`·`:127` 에서 계약 행에서 읽어
오는 칸이다. 코드는 그 칸을 `client.service.ts:77` 에서 판정에 쓸 뿐 값을 정하지 않는다. 어떤
계약에 어떤 값이 들어 있는지는 저장소 어디에도 없다.

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

| 정책 id | 관리자 | 영업대리점 | 기업담당자 | 일반회원 | 기업회원 | server | site | admin |
|---|---|---|---|---|---|---|---|---|
| client:surface-entry-points | — | — | — | — | — | 강제 지점 | — | — |
| client:credential-boundary | — | — | — | — | — | 강제 지점 | — | — |
| client:error-table-is-separate | — | — | — | — | — | 강제 지점 | — | — |
| client:contract-option-per-route | — | — | — | — | — | 강제 지점 | — | — |
| client:monthly-quota-by-person | — | — | — | — | — | 강제 지점 | — | — |
| client:synchronous-failure-split | — | — | — | — | — | 강제 지점 | — | — |
| client:entry-points-differ-below-prefix | — | — | — | — | — | 강제 지점 | — | — |
| client:car-window-cutoff | — | — | — | — | — | 강제 지점 | — | — |
| client:quota-values-are-contract-data | — | — | — | — | — | 강제 지점 | — | — |

actor 다섯 열이 전건 `—` 인 것은 이 표면의 주체가 **고객사 계약 자격 증명**이고 그것이 이
저장소의 로그인 주체 축의 원소가 아니기 때문이다. 대외 요청은 로그인하지 않으며 세션도 역할도
갖지 않는다. `—` 는 그 주체가 이 정책의 경로에 등장하지 않는다는 뜻이다.

서비스 열의 `강제 지점` 은 그 저장소에 이 표면을 정의하는 자리가 동결 기준점에 실재한다는
뜻이고 `—` 는 그 표면을 부르는 자리를 찾지 못했다는 뜻이다. **소비자 두 저장소가 전건 `—` 인
것이 이 문서의 관측이다** — 고객사는 우리 화면을 거치지 않고 서버를 직접 부른다. 관리자 화면에
있는 고객사 관련 자리는 대외 계정과 계약을 **관리**하는 내부 표면이며 이 표면의 소비가 아니다.

이 열은 기계가 대조한다. 선언 측은 관문 클래스 이름으로, 호출 측은 접두로 정규화한 대외 경로로
동결 트리를 찾아 `강제 지점` 과 `—` 를 다시 만든다. 날조된 `강제 지점` 도 날조된 `—` 도 같이
떨어진다. 소비자가 경로를 문자열 조립으로 만들면 그 호출은 이 대조에 걸리지 않는다 — 그것이 이
열의 사각지대다. actor 다섯 열은 대조 밖이며 권한 모델을 동결 트리에서 되짚는 경로가 이 조사에
없다.

## 예외·폴백

| 정책 id | 예외 조건 | 폴백 동작 | 확인 근거 |
|---|---|---|---|
| client:surface-entry-points | 등록됐으나 라우트가 없는 컨트롤러 | 로드는 되고 부를 수 있는 자리가 없다. 사장인지 준비 중인지 이 문서는 판정하지 않는다 | `client-analyze.controller.ts:8` · `settlement.controller.ts:8` |
| client:surface-entry-points | 고객사 이름이지만 관리자 관문을 다는 컨트롤러 | 대외 표면이 아니다. 관리자 화면이 부르는 내부 표면으로 읽는다 | `client-admin.controller.ts:27` |
| client:credential-boundary | 관문은 통과했으나 계약 기간 밖 | 계약 갈래로 거절된다. 자격 증명 실패와 다른 코드다 | `contract.repository.ts:135` |
| client:credential-boundary | 같은 계정에 계약 행이 여럿 | 기간 조건을 만족하는 첫 행만 쓴다. 어느 행이 첫 행인지 정렬을 지정하지 않는다 | `contract.repository.ts:136` |
| client:error-table-is-separate | 잡히지 않은 예외 | 대외 공통 실패 항목으로 접혀 나간다. 내부 표의 항목이 새지 않는다 | `client-exception.filter.ts:35` |
| client:error-table-is-separate | 같은 코드값이 내부 표에도 있음 | 표를 건너 뜻을 옮기지 않는다. 조회 키는 표와 이름의 쌍이다 | `api.constant.ts:16` · `admin.constant.ts:62` |
| client:contract-option-per-route | 계약에 그 옵션이 없음 | 계약 옵션 갈래로 거절된다. 라우트가 존재하는 것과 부를 수 있는 것은 다르다 | `client.service.ts:172` |
| client:contract-option-per-route | 어느 라우트도 요구하지 않는 옵션 | 계약에 넣을 수는 있으나 아무 조회도 열리지 않는다. 잘못인지 판정하지 않는다 | `client.dto.ts:13` |
| client:monthly-quota-by-person | 상한 스위치가 꺼져 있음 | 세지 않고 통과한다. 상한 없음이 기본값이다 | `client.service.ts:77` |
| client:monthly-quota-by-person | 같은 계약의 다른 사람 | 영향을 받지 않는다. 셈의 단위가 계약이 아니라 계약과 사람의 쌍이다 | `contract.repository.ts:159` |
| client:synchronous-failure-split | 원본 저장 실패 | 결과 칸을 먼저 갱신하고 주문 갈래로 거절한다 | `client.service.ts:106` |
| client:synchronous-failure-split | 분석 실패 | 결과 칸을 먼저 갱신하고 분석 갈래로 거절한다 | `client.service.ts:129` |
| client:entry-points-differ-below-prefix | 우회 진입점의 동시 처리 한도 초과 | 대외 표 밖의 칸 이름과 값으로 던진다. 대외 겉모양을 거치지 않는다 | `coocon-bypass.controller.ts:57` |
| client:entry-points-differ-below-prefix | 우회 진입점의 정상 응답 | 벤더 봉투가 그대로 나간다. 성공 겉모양이 씌워지지 않는다 | `coocon-bypass.controller.ts:55` |
| client:car-window-cutoff | 컷오프 이전에 만들어진 요청 | 빈 목록으로 나가고 예외가 아니다. 고객사는 실패가 아니라 결과 없음을 본다 | `client.service.ts:436` |
| client:quota-values-are-contract-data | 상한이 걸린 계약이 실재하는지 | 확인하지 않았다 — 계약 데이터를 확보하지 않아 이 문서는 그 상태를 판정하지 못한다 | `contract.repository.ts:126` |

`확인하지 않았다` 와 `막지 않는다` 는 다르다. 앞엣것은 볼 수단을 확보하지 못했다는 뜻이고
뒤엣것은 보았고 막는 장치가 없음을 확인했다는 뜻이다.

## 부록

이 절의 정책 둘은 각각 다른 이유로 본문에서 격리된다. 하나는 **아직 운영에 닿지 않았고**, 다른
하나는 **값이 코드가 아니라 계약 데이터에 있어** 판정이 서지 않는다. 본문에 두면 독자가 지금
확정된 사실로 읽는다. 배포가 닿거나 데이터를 확보해 판정이 서면 표기가 바뀌고 배치도 함께
본문으로 옮긴다.

### client:car-window-cutoff

자동차 결과 조회에는 **시점 컷오프**가 있다. 그 시점보다 앞서 만들어진 요청은 결과가 빈 목록으로
나가고 오류가 아니다. 고객사는 실패를 보지 않고 결과가 없다고 본다.

이 정책을 부록에 두는 이유는 컷오프를 만든 변경이 아직 운영에 닿지 않았기 때문이다. 지금 상태를
본문에 적으면 독자가 운영에서 이미 이렇게 동작한다고 읽는다. 운영에 닿으면 표기가 바뀌고 배치도
본문으로 옮긴다.

이용자에게 만드는 결과: 배포가 닿은 뒤 옛 요청으로 자동차 결과를 조회한 고객사는 빈 응답을 받고
그것을 장애로 오해할 수 있다. 빈 목록과 실패는 다르다.

### client:quota-values-are-contract-data

상한 스위치가 켜져 있는지, 켜져 있다면 상한이 얼마인지는 **계약 행 데이터에만** 있다. 코드는
스위치를 읽고 값을 비교할 뿐 값을 정하지 않으며, 이 조사는 그 데이터를 확보하지 않았다.

따라서 실제로 상한이 걸린 고객사가 있는지, 걸렸다면 얼마인지 이 문서는 없다고도 있다고도 말하지
않는다. 규칙이 코드에 있다는 사실과 그 규칙이 실제로 누군가에게 걸린다는 주장은 다른 것이다.

이용자에게 만드는 결과: 상한이 걸리지 않은 계약에서는 이 규칙이 전혀 관측되지 않으며, 계약을
바꾸는 것만으로 어제까지 없던 거절이 오늘 생길 수 있다.

## 연혁

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

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2025-03-11 | `0b800ee7` | 대외 진입점과 접두가 만들어지고 관문이 붙었다 |
| — | 2025-03-11 | `36ea37f6` | 관문이 계정 코드와 비밀번호 쌍으로 대외 계정 행을 찾도록 정해졌다 |
| — | 2025-03-13 | `29d5de54` | 계약 옵션 어휘가 선언되고 첫 결과 조회 라우트가 옵션을 고정했다 |
| — | 2025-03-17 | `bf64e84a` | 대외 전용 실패 표와 겉모양 두 벌이 별도 파일로 갈라졌다 |
| — | 2025-04-01 | `25e35fee` | 계약 조회가 자격 증명 쌍을 조인 조건에 다시 넣고 계약 기간 창을 걸었다 |
| — | 2025-04-02 | `c7d2b53f` | 정산 표면이 등록되면서 라우트 없는 컨트롤러가 하나 더 생겼다 |
| — | 2026-01-30 | `6da4f922` | 사람 단위 월간 상한이 계약 스위치와 함께 들어왔다 |
| — | 2026-06-30 | `eed6bf4f` | 같은 관문을 다는 우회 진입점이 접두 아래 추가됐다 |
| — | 2026-07-22 | `461857e2` | 자동차 결과 조회에 시점 컷오프가 생겼다 |
| — | 2026-07-22 | `ed93ab75` | 그 컷오프 시각이 조정됐다 |
