---
template: policy-doc@1
doc_id: 01-amounts-and-quotas
title: 금액·수량 한도 — 이름이 아니라 강제 층이 정책을 만든다
config: ../_meta/template-config.yaml
last_verified: 4d798f55
policies:
  - id: limits:money-cap-is-column-width
    status: active-prod
    evidence: [S-constant, S-schema]
    limitations:
      - 이 상한을 업무 판단으로 정한 문서나 커밋 메시지를 찾지 못했다. 값이 저장 칸이 담을 수 있는 최댓값과 같다는 것만 관측했고 그 일치가 의도였는지는 판정하지 않는다
    anchors:
      - insuniverse-server/apps/api/src/modules/money/money.dto.ts:97
      - insuniverse-server/apps/api/src/modules/money/money.dto.ts:98
      - insuniverse-server/drizzle/schema.ts:402
      - insuniverse-server/drizzle/schema.ts:1503
      - insuniverse-server/drizzle/schema.ts:1504
      - insuniverse-server/drizzle/schema.ts:1505
      - insuniverse-server/drizzle/schema.ts:2829
      - insuniverse-server/drizzle/schema.ts:2931
      - insuniverse-server/drizzle/schema.ts:3011
  - id: limits:balance-cap-at-write
    status: active-prod
    evidence: [S-contract, S-guard]
    limitations:
      - 등록한 강제 지점은 기업 충전 승인 경로 하나다. 같은 값의 상한이 서비스 계층 가드에도 있어 다른 쓰기 경로 다수가 그것을 지나고, 그 가드를 거치지 않는 경로는 칸 제약만 남는다. 경로별 분포는 이 문서가 완주하지 않았다
    anchors:
      - insuniverse-server/apps/api/src/modules/money/money.repository.ts:486
      - insuniverse-server/apps/api/src/modules/money/money.dto.ts:187
      - insuniverse-server/apps/api/src/modules/money/money.dto.ts:186
  - id: limits:charge-floor-three-layers
    status: active-prod
    evidence: [S-constant, S-contract, S-guard]
    limitations:
      - 관리자 화면 상수만 이름을 갖는다. 서버 두 자리는 이름 없는 숫자라 상수 색인이 담지 못했고, 색인만 보고 이 정책을 쓰면 강제 층이 통째로 빠진다
    anchors:
      - insuniverse-admin/src/pages/Corp/Money/ChargeRequest/MoneyChargeRequestPage.tsx:11
      - insuniverse-server/apps/api/src/modules/money/money.dto.ts:327
      - insuniverse-server/apps/api/src/modules/money/money.dto.ts:864
      - insuniverse-server/apps/api/src/modules/money/money.service.ts:636
  - id: limits:age-floor
    status: active-prod
    evidence: [S-constant, S-guard]
    anchors:
      - insuniverse-server/apps/api/src/helpers/identity.helper.ts:7
      - insuniverse-server/apps/api/src/helpers/identity.helper.ts:82
  - id: limits:attachment-count
    status: active-prod
    evidence: [S-constant, S-component]
    limitations:
      - 같은 상한을 서버가 다시 확인하는 자리를 동결 기준점에서 찾지 못했다. 화면을 거치지 않는 요청에 이 상한이 걸리는지는 이 문서가 판정하지 않는다
    anchors:
      - insuniverse-site/front/src/pages/board/upsert/UpsertBoardPage.tsx:28
      - insuniverse-site/front/src/pages/loss/UpsertLossPage.tsx:32
---

# 금액·수량 한도 — 이름이 아니라 강제 층이 정책을 만든다

이 문서는 **금액과 개수의 상한·하한**만 다룬다. 얼마까지 넣을 수 있는가, 얼마부터 받는가, 몇 개까지
붙일 수 있는가 세 축이다.

읽는 사람이 여기서 얻어야 하는 것은 **이름이 정책을 정하지 않는다**는 사실이다. 업무 한도처럼 이름이
붙은 값이 실은 저장 칸이 담을 수 있는 최댓값 그대로이고, 반대로 진짜 업무 판단으로 정해진 값은 이름
없는 숫자로 세 군데에 흩어져 있어 상수 목록만 보면 한 군데밖에 보이지 않는다. 한도를 읽을 때 봐야
하는 것은 값의 이름이 아니라 **그 값이 어느 층에서 걸리는가**다.

여기 없는 것: 시간 한도와 빈도 한도, 칸 형과 기본값의 정의, 입력 형식과 필수 여부, 한도를 넘었을 때
남는 상태 낱말, 정산과 잠금의 서사, 기능 게이트의 운영 활성 여부. 각각 다른 문서의 소유이며 이
문서는 그 자리를 가리키기만 한다.

## 정책 (기획)

### limits:money-cap-is-column-width

머니 금액에는 두 상한이 있다. 한 번에 움직일 수 있는 **변동액**이 2,147,483,647원까지이고, 계정에
쌓아 둘 수 있는 **잔액**이 4,294,967,295원까지다.

두 값은 업무 판단으로 정해진 액수가 아니다. 잔액을 담는 데이터베이스 칸이 음수를 쓰지 않는 정수형
이고, 그 칸이 담을 수 있는 최댓값이 곧 잔액 상한이다. 변동액 쪽은 음수를 함께 쓰는 정수형의
최댓값이며 그래서 잔액 상한의 절반쯤이다. **저장 칸 폭에서 나온 상한은 하나뿐이다** — 잔액 쪽이고,
변동액 상한은 그 칸에 넣을 값을 만드는 계산의 폭이다.

이 구별이 만드는 결과: 이 두 수는 정책 회의로 바꿀 수 있는 값이 아니다. 올리려면 칸 형을 먼저
바꿔야 하고, 그것은 저장 구조의 변경이다. 반대로 두 수를 업무 한도로 읽고 "왜 이 금액인가"를 물으면
답이 없다 — 답은 업무가 아니라 저장 구조에 있다.

이용자에게 만드는 결과: 정상 거래에서 이 상한에 닿는 일은 사실상 없다. 닿는다면 거의 언제나 자릿수를
잘못 입력한 경우이며, 그때 보이는 것은 금액이 너무 크다는 거절이다.

### limits:balance-cap-at-write

상한이 어디서 걸리는지는 두 값이 다르다. **변동액은 요청이 들어오는 문턱에서** 걸리고, **잔액은
쓰기 직전에** 걸린다.

변동액은 요청 본문을 받아들이는 계약에 상한과 하한이 함께 붙어 있다. 양수 방향으로
2,147,483,647원까지, 음수 방향으로 같은 크기까지다. 회수와 차감이 같은 칸을 음수로 쓰기 때문에
하한이 대칭으로 걸린다.

잔액은 다르다. 요청을 받을 때는 보지 않고, 바꾼 뒤의 잔액을 먼저 계산해 그것이 4,294,967,295원을
넘는지 확인한 다음에야 저장한다. 그래서 요청 자체는 정상 범위인데 더한 결과가 상한을 넘어 거절되는
경우가 생긴다.

이용자에게 만드는 결과: 같은 금액이 어제는 통과하고 오늘은 거절될 수 있다. 거절의 원인은 넣은
금액이 아니라 이미 들고 있던 잔액이다.

### limits:charge-floor-three-layers

충전은 **10,000원부터** 받는다. 이 값은 앞의 두 상한과 성격이 다르다 — 저장 구조에서 나온 것이
아니라 업무 판단이다.

이 문서가 이 정책을 실은 이유는 값 때문이 아니라 걸리는 자리 때문이다.
**충전 최소액을 거는 층은 셋이다.** 관리자 화면이 입력 단계에서 한 번, 서버가 요청 본문을 받아들이는
계약에서 한 번, 서버 업무 처리가 저장 직전에 한 번 건다. 셋 중 이름이 붙은 것은 화면 쪽 하나뿐이고
서버 두 자리는 이름 없는 숫자다.

이 구별이 만드는 결과: 상수 목록을 근거로 이 한도를 설명하면 "화면에서만 막힌다" 는 틀린 확신을
준다. 실제로는 화면을 우회해 서버를 직접 불러도 같은 값에서 막힌다. 반대로 화면 상수만 고치면
아무것도 바뀌지 않는다 — 서버 두 층이 그대로 남는다.

이용자에게 만드는 결과: 기업 충전 요청과 개인 충전이 같은 하한을 쓴다. 더 적은 금액을 넣으면 화면이
먼저 막고, 화면을 거치지 않아도 서버가 같은 문장으로 막는다.

### limits:age-floor

만 14세 미만은 진료 정보 인증을 이용할 수 없다. 주민등록번호에서 생년월일을 읽어 만 나이를 계산하고
하한 미만이면 대기열과 외부 인증에 들어가기 전에 거른다.

이 값은 이 문서에서 몇 안 되는 **진짜 업무 한도**다. 저장 구조와 무관하고, 선언 자리의 주석이 정책이
바뀌면 이 값만 조정하라고 적고 있다.

거르는 위치도 정책의 일부다. 외부 인증을 부르기 전에 걸러서, 나이 때문에 실패할 요청이 대기열
자리와 외부 호출 비용을 쓰지 않게 한다.

이용자에게 만드는 결과: 연령 제한으로 거절될 때 보이는 것은 시스템 오류가 아니라 이용할 수 없다는
안내다. 대기 없이 즉시 나온다.

### limits:attachment-count

등록 폼의 첨부 파일은 **5개까지**다. 문의 등록과 손해사정 등록 두 화면이 같은 상한을 쓴다.

이 정책이 앞의 셋과 다른 점은 **강제 층이 하나뿐**이라는 것이다. 두 화면이 각각 상수를 들고 파일을
더할 때 개수를 세지만, 같은 상한을 서버가 다시 확인하는 자리를 동결 기준점에서 찾지 못했다.

이 구별이 만드는 결과: 화면을 거치는 한 상한은 지켜지지만, 화면을 거치지 않는 요청에 대해 이 문서는
막힌다고도 막히지 않는다고도 말하지 않는다. 확인할 자리를 찾지 못한 것과 없음을 확인한 것은 다르다.

이용자에게 만드는 결과: 여섯 번째 파일을 고르면 더해지지 않고 안내가 뜬다. 두 화면의 동작이 같다.

## 강제 지점 (개발)

### limits:money-cap-is-column-width

두 상한 상수는 머니 요청·응답 계약 파일 머리에 선언돼 있고, 잔액을 담는 칸 일곱 자리가 전부 부호
없는 정수형이다. 상수 선언 자체는 강제가 아니다 — 그래서 층이 `—` 다. 이 정책에서 실제로 값을
가두는 층은 칸 형이다.

- 슬롯 — MAX_MONEY_CHANGE_AMOUNT: 값 `2147483647` · 층 `—` · 출처 `고정값` · 앵커 `insuniverse-server/apps/api/src/modules/money/money.dto.ts:97`
- 슬롯 — MAX_MONEY_BALANCE_AMOUNT: 값 `4294967295` · 층 `—` · 출처 `고정값` · 앵커 `insuniverse-server/apps/api/src/modules/money/money.dto.ts:98`
- 슬롯 — agc_money: 값 `agc_money` · 층 `DB 제약` · 출처 `고정값` · 앵커 `insuniverse-server/drizzle/schema.ts:402`
- 슬롯 — mem_money: 값 `mem_money` · 층 `DB 제약` · 출처 `고정값` · 앵커 `insuniverse-server/drizzle/schema.ts:1503`
- 슬롯 — mem_purchase_money: 값 `mem_purchase_money` · 층 `DB 제약` · 출처 `고정값` · 앵커 `insuniverse-server/drizzle/schema.ts:1504`
- 슬롯 — mem_agency_money: 값 `mem_agency_money` · 층 `DB 제약` · 출처 `고정값` · 앵커 `insuniverse-server/drizzle/schema.ts:1505`
- 슬롯 — mcp_balance: 값 `mcp_balance` · 층 `DB 제약` · 출처 `고정값` · 앵커 `insuniverse-server/drizzle/schema.ts:2829`
- 슬롯 — mpy_money: 값 `mpy_money` · 층 `DB 제약` · 출처 `고정값` · 앵커 `insuniverse-server/drizzle/schema.ts:2931`
- 슬롯 — mwl_balance: 값 `mwl_balance` · 층 `DB 제약` · 출처 `고정값` · 앵커 `insuniverse-server/drizzle/schema.ts:3011`

### limits:balance-cap-at-write

등록한 잔액 상한 비교 자리는 **기업 충전 승인 경로의 저장소 질의 하나**다 — 갱신 후 값을 계산해
비교한 뒤 거절한다. 이 문서는 그 한 경로만 강제 지점으로 등록하며, 모든 잔액 쓰기가 같은 층에서
같은 거절 형태를 갖는다고 주장하지 않는다. 같은 값의 상한이 서비스 계층 가드에도 있어 다른 쓰기
경로 다수가 그것을 지나고, 그 가드를 거치지 않는 경로는 칸 제약만 남는다 — 경로별 분포는 정책
한계로 기록한다. 변동액 상·하한은 요청 본문 계약의 검증 데코레이터 인자에 있다.

- 슬롯 — 잔액 상한 비교: 값 `4294967295` · 층 `저장소 질의` · 출처 `고정값` · 앵커 `insuniverse-server/apps/api/src/modules/money/money.repository.ts:486`
- 슬롯 — 변동액 상한 인자: 값 `2147483647` · 층 `입력 계약 데코레이터` · 출처 `고정값` · 앵커 `insuniverse-server/apps/api/src/modules/money/money.dto.ts:187`
- 슬롯 — 변동액 하한 인자: 값 `-2147483647` · 층 `입력 계약 데코레이터` · 출처 `고정값` · 앵커 `insuniverse-server/apps/api/src/modules/money/money.dto.ts:186`

### limits:charge-floor-three-layers

같은 값이 세 층에 각각 있다. 관리자 화면 상수만 이름을 갖고, 서버 두 자리는 데코레이터 인자와 비교
연산 우변의 이름 없는 숫자다. 상수 색인이 대문자 상수 선언만 모으므로 서버 두 자리는 그 색인에 실려
있지 않다.

- 슬롯 — MIN_CHARGE_AMOUNT: 값 `10000` · 층 `클라이언트 상수` · 출처 `고정값` · 앵커 `insuniverse-admin/src/pages/Corp/Money/ChargeRequest/MoneyChargeRequestPage.tsx:11`
- 슬롯 — 기업 충전요청 최소 인자: 값 `10000` · 층 `입력 계약 데코레이터` · 출처 `고정값` · 앵커 `insuniverse-server/apps/api/src/modules/money/money.dto.ts:327`
- 슬롯 — 회원 충전 최소 인자: 값 `10000` · 층 `입력 계약 데코레이터` · 출처 `고정값` · 앵커 `insuniverse-server/apps/api/src/modules/money/money.dto.ts:864`
- 슬롯 — 충전요청 최소 가드: 값 `10000` · 층 `서비스 가드` · 출처 `고정값` · 앵커 `insuniverse-server/apps/api/src/modules/money/money.service.ts:636`

### limits:age-floor

하한 상수와 그것을 무는 분기가 같은 파일에 있다. 분기는 예외를 던지지 않고 실패 결과를 돌려주며,
호출자가 그것을 받아 대기열 진입 전에 끊는다.

- 슬롯 — MIN_AGE: 값 `14` · 층 `—` · 출처 `고정값` · 앵커 `insuniverse-server/apps/api/src/helpers/identity.helper.ts:7`
- 슬롯 — 연령 하한 가드: 값 `14` · 층 `서비스 가드` · 출처 `고정값` · 앵커 `insuniverse-server/apps/api/src/helpers/identity.helper.ts:82`

### limits:attachment-count

두 화면이 각각 상수를 선언하고 각자 개수를 센다. 공유 상수가 아니라 같은 값의 복제다.

- 슬롯 — 문의 첨부 MAX_FILES: 값 `5` · 층 `클라이언트 상수` · 출처 `고정값` · 앵커 `insuniverse-site/front/src/pages/board/upsert/UpsertBoardPage.tsx:28`
- 슬롯 — 손해사정 첨부 MAX_FILES: 값 `5` · 층 `클라이언트 상수` · 출처 `고정값` · 앵커 `insuniverse-site/front/src/pages/loss/UpsertLossPage.tsx:32`

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

| 정책 id | 관리자 | 영업대리점 | 기업담당자 | 일반회원 | 기업회원 | server | site | admin |
|---|---|---|---|---|---|---|---|---|
| limits:money-cap-is-column-width | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | — | — |
| limits:balance-cap-at-write | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | — | — |
| limits:charge-floor-three-layers | 적용 | — | 적용 | 적용 | 적용 | 강제 지점 | — | 강제 지점 |
| limits:age-floor | — | — | — | 적용 | 적용 | 강제 지점 | — | — |
| limits:attachment-count | — | — | 적용 | 적용 | 적용 | — | 강제 지점 | — |

서비스 열의 `강제 지점` 은 그 저장소에 이 정책을 무는 자리가 동결 기준점에 실재한다는 뜻이고 `—` 는
그런 자리를 찾지 못했다는 뜻이다. 이 세 열은 기계가 대조한다 — 그 저장소의 동결 blob 에서 정책이
선언한 구조가 실제로 그 행에 있는지를 다시 보고, 날조된 `강제 지점` 도 날조된 `—` 도 같이 떨어진다.

actor 다섯 열은 대조 밖이다. 이 한도들이 어느 로그인 주체의 경로에 등장하는지를 동결 트리에서
되짚을 경로가 이 조사에 없다 — 권한 모델은 이 문서의 축이 아니다. 그 다섯 열은 읽는 사람을 위한
안내이며 기계가 보증하지 않는다.

## 예외·폴백

| 정책 id | 예외 조건 | 폴백 동작 | 확인 근거 |
|---|---|---|---|
| limits:money-cap-is-column-width | 상한을 넘는 값이 계산됨 | 저장 전에 거절된다. 칸에 잘려 들어가지 않는다 | `money.repository.ts:486` |
| limits:balance-cap-at-write | 요청은 정상인데 더한 잔액이 상한 초과 | 요청 갈래가 아니라 잔액 갈래로 거절된다 | `money.repository.ts:486` |
| limits:balance-cap-at-write | 음수 방향 변동액 | 같은 크기의 하한이 대칭으로 걸린다 | `money.dto.ts:186` |
| limits:charge-floor-three-layers | 화면을 거치지 않은 요청 | 서버 계약과 서비스 가드가 같은 값에서 막는다 | `money.dto.ts:327` · `money.service.ts:636` |
| limits:charge-floor-three-layers | 정수가 아닌 금액 | 최소액 판정과 같은 분기에서 함께 거절된다 | `money.service.ts:636` |
| limits:age-floor | 생년월일을 읽을 수 없는 입력 | 연령 판정 이전 단계에서 거절된다. 연령 갈래가 아니다 | `identity.helper.ts:82` |
| limits:age-floor | 하한 미만 | 대기열과 외부 인증에 들어가기 전에 끊는다 | `identity.helper.ts:82` |
| limits:attachment-count | 상한을 넘겨 파일을 고름 | 더해지지 않고 안내만 뜬다. 요청이 나가지 않는다 | `UpsertBoardPage.tsx:28` |
| limits:attachment-count | 화면을 거치지 않은 업로드 | 같은 상한을 서버가 다시 확인하는 자리를 찾지 못했다 — 판정하지 않는다 | `UpsertLossPage.tsx:32` |

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

## 연혁

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

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2025-01-03 | `8fc12e01` | 문의 등록 폼에 첨부 개수 상한이 생겼다 |
| — | 2025-03-31 | `6347fe05` | 손해사정 등록 폼이 같은 첨부 상한을 복제해 따랐다 |
| — | 2025-11-24 | `52362e3b` | 기업 잔액 칸이 부호 없는 정수형으로 정리됐다 |
| — | 2025-11-24 | `c543c6c9` | 회원 잔액 세 칸이 머니 이름으로 바뀌며 같은 칸 형이 굳었다 |
| — | 2026-01-12 | `46efac63` | 충전·결제·지갑 잔액 칸과 회원 충전 하한이 4차 고도화에서 정리됐다 |
| — | 2026-06-07 | `aa901f69` | 연령 하한이 대기열·외부 인증 진입 전 사전검증으로 들어왔다 |
| — | 2026-07-06 | `397f7453` | 금액 상한 두 상수와 변동액 데코레이터·잔액 쓰기 비교가 함께 섰다 |
| — | 2026-07-06 | `0e0af59f` | 관리자 충전 요청 화면이 같은 하한·상한 상수를 들었다 |
