---
template: policy-doc@1
doc_id: 01-input-validation
title: 입력 검증 계약 — 요청 값이 통과하는 조건
config: ../_meta/template-config.yaml
last_verified: 4d798f55
policies:
  - id: input:declared-fields-only
    status: active-prod
    evidence: [S-contract, S-guard]
    anchors:
      - insuniverse-server/apps/api/src/pipes/validation.pipe.ts:16
      - insuniverse-server/apps/api/src/pipes/validation.pipe.ts:17
      - insuniverse-server/apps/api/src/app.module.ts:188
      - insuniverse-server/apps/api/src/constants/client.constant.ts:31
  - id: input:type-coercion
    status: active-prod
    evidence: [S-contract, S-guard]
    anchors:
      - insuniverse-server/apps/api/src/pipes/validation.pipe.ts:18
      - insuniverse-server/apps/api/src/modules/common/common.dto.ts:10
      - insuniverse-server/apps/api/src/constants/client.constant.ts:31
  - id: input:first-failure-only
    status: active-prod
    evidence: [S-contract, S-guard]
    limitations:
      - 여러 항목이 동시에 틀렸을 때 어느 항목이 첫 건이 되는지는 검증기가 오류를 모으는 순서에 달려 있고, 그 순서는 이 문서가 확인한 범위 밖이다
    anchors:
      - insuniverse-server/apps/api/src/pipes/validation.pipe.ts:26
      - insuniverse-server/apps/api/src/pipes/validation.pipe.ts:36
      - insuniverse-server/apps/api/src/pipes/validation.pipe.ts:37
      - insuniverse-server/apps/api/src/constants/client.constant.ts:31
  - id: input:length-cap
    status: active-prod
    evidence: [S-contract, S-guard]
    anchors:
      - insuniverse-server/apps/api/src/pipes/validation.pipe.ts:28
      - insuniverse-server/apps/api/src/pipes/validation.pipe.ts:45
      - insuniverse-server/apps/api/src/modules/auth/user/auth.dto.ts:35
      - insuniverse-server/apps/api/src/modules/agency/organization/organization.dto.ts:69
      - insuniverse-server/apps/api/src/constants/client.constant.ts:32
  - id: input:format-rule
    status: active-prod
    evidence: [S-contract, S-guard]
    anchors:
      - insuniverse-server/apps/api/src/modules/auth/user/auth.dto.ts:36
      - insuniverse-server/apps/api/src/modules/auth/user/auth.dto.ts:98
      - insuniverse-server/apps/api/src/modules/auth/user/auth.dto.ts:125
      - insuniverse-server/apps/api/src/modules/analyze/analyze.dto.ts:988
      - insuniverse-server/apps/api/src/constants/client.constant.ts:31
  - id: input:allowed-value-set
    status: active-prod
    evidence: [S-contract, S-guard]
    anchors:
      - insuniverse-server/apps/api/src/modules/common/common.dto.ts:27
      - insuniverse-server/apps/api/src/constants/client.constant.ts:31
  - id: input:optional-fields
    status: active-prod
    evidence: [S-contract, S-guard]
    anchors:
      - insuniverse-server/apps/api/src/modules/common/common.dto.ts:9
      - insuniverse-server/apps/api/src/modules/common/common.dto.ts:21
      - insuniverse-server/apps/api/src/constants/client.constant.ts:31
  - id: input:numeric-bound
    status: active-prod
    evidence: [S-contract, S-guard]
    limitations:
      - 어느 항목에 어떤 값이 걸리는지는 이 문서가 들지 않는다. 수치 자체는 한도 축 문서의 소유다
    anchors:
      - insuniverse-server/apps/api/src/modules/agency/contract/contract-admin.dto.ts:36
      - insuniverse-server/apps/api/src/modules/agency/contract/contract-admin.dto.ts:37
      - insuniverse-server/apps/api/src/modules/agency/organization/organization.dto.ts:41
      - insuniverse-server/apps/api/src/constants/client.constant.ts:31
  - id: input:loss-subject-format
    status: staged-only
    evidence: [S-contract, A-release]
    limitations:
      - 미도달 판정의 근거는 두 앵커의 최종 변경 커밋 하나다. 그 행이 뒤에 다른 커밋으로 밀리면 같은 규칙이 도달로 뒤집힐 수 있다
    anchors:
      - insuniverse-server/apps/api/src/modules/loss-analysis/loss-analysis.dto.ts:38
      - insuniverse-server/apps/api/src/modules/loss-analysis/loss-analysis.dto.ts:41
---

# 입력 검증 계약 — 요청 값이 통과하는 조건

이 문서는 **요청이 서버에 받아들여지는 조건**만 다룬다. 어떤 값이 거부되는가, 거부되면 무엇이
돌아오는가 중 앞쪽이 여기고 뒤쪽은 응답 계약 문서(`02-api-response.md`)가 든다. 두 문서는
같은 자리에서 만난다 — 입력 검증 실패가 어떤 필드 이름으로 나가는지는 그쪽 문서의 정책이다.

개별 항목의 상한값·시간·빈도 같은 수치 자체와, 데이터베이스가 지는 제약은 여기 없다.

## 정책 (기획)

### input:declared-fields-only

요청에 **미리 정해두지 않은 항목**이 섞여 있으면 그 요청은 거부된다. 모르는 항목을 조용히
버리고 나머지를 처리하는 것이 아니라, 요청 전체가 실패로 끝난다.

이 규칙은 어느 표면에서나 같다. 관리자 화면이 부르는 경로든 고객사가 부르는 경로든 구별하지
않는다.

### input:type-coercion

주소창 질의처럼 값이 언제나 글자로 도착하는 경로가 있다. 숫자로 받기로 되어 있는 자리에 글자
"12" 가 오면, 서버는 먼저 그것을 숫자로 바꾼 다음 검사한다. 바꿀 수 없는 값은 그 자리에서
입력 오류가 된다.

따라서 "숫자만 넣어야 통과한다" 가 아니라 "숫자로 읽히면 통과한다" 가 실제 규칙이다.

### input:first-failure-only

여러 항목이 한꺼번에 틀려도 **안내는 한 건만** 나간다. 틀린 항목 전부를 한 번에 알려주지
않는다. 항목 안에 다시 항목이 들어 있는 구조라면 가장 깊은 첫 자식까지 내려가서 그 이름을
알린다.

이용자 입장에서는 한 번에 하나씩 고치며 여러 번 시도하게 된다.

### input:length-cap

글자 수 상한이 정해진 항목은 그 수를 넘기면 거부되고, 안내에 **항목 이름과 제한값**이 함께
나간다. 상한이 없는 항목은 이 규칙에 걸리지 않는다.

안내 문구는 제한값을 바이트 단위로 알린다. 실제로 세는 것은 글자 수다 — 한글처럼 한 글자가
여러 바이트인 값에서는 안내에 적힌 단위와 실제로 세는 단위가 같지 않다. 여기서는 관측한
그대로만 적는다.

### input:format-rule

아이디·비밀번호·인증번호·생년월일처럼 **모양이 정해진 항목**은 그 모양에 맞지 않으면
거부된다. 글자 종류, 자릿수, 섞어 써야 하는 문자 종류가 항목마다 다르다.

이 규칙에 걸린 항목은 길이 상한과 달리 제한값을 안내에 싣지 않는다. 어떤 모양이어야 하는지는
안내가 아니라 화면 쪽 설명이 진다.

### input:allowed-value-set

**정해진 값 목록에서 고르는 항목**은 목록 밖의 값을 받지 않는다. 목록에 없는 값을 보내면
거부되며, 대소문자가 다른 값도 다른 값으로 본다.

### input:optional-fields

**선택 항목은 아예 보내지 않아도 된다.** 다만 보냈다면 그 항목에 걸린 다른 규칙을 모두
지켜야 한다. 보내지 않는 것과 비운 채로 보내는 것은 같지 않다 — 뒤쪽은 검사를 받는다.

선택 항목에 기본값이 정해져 있는 자리도 있다. 그 경우 보내지 않으면 기본값이 쓰인다.

### input:numeric-bound

숫자 항목 중 일부는 **아래 위 경계**가 정해져 있고, 그 범위를 벗어난 값은 거부된다. 어느
항목에 어떤 경계가 걸려 있는지는 이 문서가 들지 않는다 — 수치 자체는 한도 축 문서의 소유다.

## 강제 지점 (개발)

### input:declared-fields-only

- 강제 방식: 전역 파이프. `insuniverse-server/apps/api/src/app.module.ts:188` 이 `ClassValidatorPipe` 를 `APP_PIPE` 로 등록해 모든 라우트가 지나간다.
- 판정: `validation.pipe.ts:16` `whitelist: true` 로 미선언 속성을 벗기고, `:17` `forbidNonWhitelisted: true` 로 벗길 것이 있으면 예외로 올린다. 두 플래그가 같이 있어야 "거부" 가 되고 `:16` 만 있으면 "조용히 제거" 다.
- 실패 응답 상수: `insuniverse-server/apps/api/src/constants/client.constant.ts:31`.

### input:type-coercion

- 강제 방식: 전역 파이프 옵션. `validation.pipe.ts:18` `transform: true`.
- 항목 단위 변환기: `insuniverse-server/apps/api/src/modules/common/common.dto.ts:10` — 목록 조회 공통 입력이 `@Transform` 으로 글자를 정수로 바꾼다.
- 변환 실패는 검증 실패로 떨어지고 같은 상수를 탄다: `client.constant.ts:31`.

### input:first-failure-only

- 강제 방식: 예외 생성기. `validation.pipe.ts:26` 이 오류 목록에서 첫 건만 고른다.
- 중첩 하강: `:36-37` — 자식이 있으면 첫 자식으로 계속 내려간다. 종료 조건은 자식이 없는 노드다.
- 그 한 건이 상수로 포장돼 나간다: `client.constant.ts:31`.

### input:length-cap

- 강제 방식: 항목 데코레이터 + 전용 분기. `validation.pipe.ts:28` 이 `maxLength` 제약이 걸린 오류를 길이 오류 경로로 보낸다.
- 제한값 추출: `:45` — 제약 문구에서 숫자만 뽑아 안내에 채운다.
- 적용 예: `insuniverse-server/apps/api/src/modules/auth/user/auth.dto.ts:35` · `insuniverse-server/apps/api/src/modules/agency/organization/organization.dto.ts:69`.
- 안내 상수: `client.constant.ts:32` — 문면이 `#제한Byte` 자리를 갖는다. 채워지는 값은 `@MaxLength` 인자이며 그 데코레이터는 문자 수를 센다.

### input:format-rule

- 강제 방식: 항목 데코레이터 `@Matches`.
- 적용 예: `auth.dto.ts:36`(로그인 아이디) · `auth.dto.ts:98`(비밀번호) · `auth.dto.ts:125`(인증번호) · `insuniverse-server/apps/api/src/modules/analyze/analyze.dto.ts:988`(생년월일).
- 실패 응답 상수: `client.constant.ts:31`. 길이 오류 분기를 타지 않으므로 제한값 자리가 없다.

### input:allowed-value-set

- 강제 방식: 항목 데코레이터 `@IsIn`.
- 적용 예: `common.dto.ts:27` — 목록 조회 정렬 방향.
- 실패 응답 상수: `client.constant.ts:31`.

### input:optional-fields

- 강제 방식: 항목 데코레이터 `@IsOptional`.
- 적용 예: `common.dto.ts:9` — 같은 속성에 기본값이 함께 선언돼 있어 미전송 시 그 값이 쓰인다. `common.dto.ts:21` — 기본값 없이 선택인 자리이며 미전송 시 속성이 없는 채로 남는다.
- 전송했으나 규칙을 어긴 경우의 응답 상수: `client.constant.ts:31`.

### input:numeric-bound

- 강제 방식: 항목 데코레이터 `@Min` · `@Max`.
- 적용 예: `insuniverse-server/apps/api/src/modules/agency/contract/contract-admin.dto.ts:36` · `:37` · `insuniverse-server/apps/api/src/modules/agency/organization/organization.dto.ts:41`.
- 경계값은 여기 옮기지 않는다 — 인자 값의 소유는 한도 축이다.
- 실패 응답 상수: `client.constant.ts:31`.

### input:loss-subject-format

- 강제 방식: 항목 데코레이터. `insuniverse-server/apps/api/src/modules/loss-analysis/loss-analysis.dto.ts:38` 글자 수 상한 · `:41` 연락처 모양.
- 두 행의 최종 변경 커밋은 동결 기준점 자신이며, 그 커밋은 `docs/_meta/index/releases.yaml` 의 미도달 집합에 있다. 그래서 이 정책만 부록으로 격리된다.

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

| 정책 id | 관리자 | 영업대리점 | 기업담당자 | 일반회원 | 기업회원 | server | site | admin |
|---|---|---|---|---|---|---|---|---|
| input:declared-fields-only | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| input:type-coercion | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| input:first-failure-only | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| input:length-cap | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| input:format-rule | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| input:allowed-value-set | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| input:optional-fields | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| input:numeric-bound | 적용 | 미적용 | 적용 | 미적용 | 미적용 | 강제 지점 | — | 호출 경로 |
| input:loss-subject-format | 적용 | 적용 | — | — | — | 강제 지점 | — | 호출 경로 |

`적용` = 그 actor 의 요청이 이 규칙을 지난다. `미적용` = 경로에 등장하나 규칙이 걸리는 항목이
없다. `—` = 그 actor 또는 서비스가 이 정책의 경로에 등장하지 않는다.

앞 일곱 정책이 전 actor 에 `적용` 인 것은 전역 파이프가 라우트를 가리지 않기 때문이다.
`input:numeric-bound` 와 `input:loss-subject-format` 은 걸린 항목이 특정 업무 입력에만 있어
좁다.

## 예외·폴백

| 정책 id | 예외 조건 | 폴백 동작 | 확인 근거 |
|---|---|---|---|
| input:declared-fields-only | 없음 — 전역 등록이라 라우트별 해제 지점을 찾지 못했다 | — | `app.module.ts:188` · `validation.pipe.ts:17` |
| input:type-coercion | 없음 — 전역 옵션이며 항목별 해제 지점을 찾지 못했다 | — | `validation.pipe.ts:18` |
| input:first-failure-only | 없음 | — | `validation.pipe.ts:26` |
| input:length-cap | 항목에 글자 수 상한이 선언되지 않음 | 길이 오류 분기를 타지 않고 일반 입력 오류로 처리 | `validation.pipe.ts:28` |
| input:length-cap | 상한 제약 문구에서 숫자를 뽑지 못함 | 제한값 자리를 빈 문자열로 두고 안내를 낸다 | `validation.pipe.ts:45-47` |
| input:format-rule | 항목에 모양 규칙이 선언되지 않음 | 형 검사만 받는다 | `auth.dto.ts:36` |
| input:allowed-value-set | 항목에 허용 목록이 선언되지 않음 | 형 검사만 받는다 | `common.dto.ts:27` |
| input:optional-fields | 항목을 보내지 않음 | 나머지 규칙을 건너뛴다. 클래스 속성 기본값이 있으면 그 값이 쓰인다 | `common.dto.ts:9` · `common.dto.ts:21` |
| input:numeric-bound | 항목에 경계가 선언되지 않음 | 형 검사만 받는다 | `contract-admin.dto.ts:36-37` |
| input:loss-subject-format | 해당 업무 입력이 아닌 경로 | 이 규칙이 걸리지 않는다 | `loss-analysis.dto.ts:38` · `:41` |

`없음` 세 건은 확인해서 없는 것이다 — 전역 등록 한 자리와 파이프 생성자 한 자리를 읽었고 그
둘을 라우트별로 되돌리는 지점을 동결 기준점에서 찾지 못했다. 찾지 못한 것과 존재하지 않는
것의 차이는 이 문장이 진다.

## 부록

이 절의 정책은 **아직 운영에 도달하지 않았다.** 규칙은 동결 기준점의 코드에 있으나 그 코드를
들여온 커밋이 운영 도달 집합 밖이라, 본문에 두면 독자가 지금 걸려 있는 규칙으로 읽는다.
도달이 확인되면 표기가 바뀌고 배치도 함께 본문으로 옮긴다.

### input:loss-subject-format

손해사정 분석을 대신 받을 사람을 초대할 때, 받는 사람의 이름과 연락처에 규칙이 걸린다. 이름은
글자 수 상한을 넘길 수 없고, 연락처는 국내 휴대전화 번호 모양이어야 한다.

같은 업무의 다음 단계에서 받는 생년월일과 인증 수단 이름에도 규칙이 걸리지만, 그 두 항목은
초대 단계가 아니라 진행 단계의 입력이다.

## 연혁

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

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2024-09-12 | `5062978a` | 회원 가입 입력에 글자 수 상한이 붙었다 |
| — | 2024-10-29 | `27726bb2` | 목록 조회 공통 입력에 선택 항목과 정렬 방향 허용 목록이 생겼다 |
| — | 2024-11-12 | `c7b37c9a` | 목록 조회 공통 입력의 숫자 항목에 글자→숫자 변환기가 붙었다 |
| — | 2024-12-18 | `23f224cc` | 로그인 아이디에 모양 규칙이 추가됐다 |
| — | 2025-03-17 | `7241229d` | 전역 입력 검증 파이프 도입 — 미선언 항목 거부·형 변환·첫 실패만 보고·길이 오류 전용 안내가 한 자리에서 결정된다 |
| — | 2026-04-29 | `55d897a4` | 비밀번호 모양 규칙을 화면 쪽과 같게 맞췄다 |
| — | 2026-07-08 | `2780ce15` | 기업 계약 회차 입력에 숫자 경계가 붙었다 |
| — | 2026-08-05 | `4d798f55` | 손해사정 초대 입력에 글자 수 상한과 연락처 모양 규칙이 추가됐다 (운영 미도달) |
