---
template: policy-doc@1
doc_id: 02-api-response
title: 응답 계약 — 성공과 실패가 돌아오는 형태
config: ../_meta/template-config.yaml
last_verified: 4d798f55
policies:
  - id: response:no-global-envelope
    status: active-prod
    evidence: [S-guard, A-comment]
    anchors:
      - insuniverse-server/apps/api/src/main.ts:25
      - insuniverse-server/apps/api/src/modules/agency/organization/organization-admin.controller.ts:37
      - insuniverse-server/apps/api/src/modules/agency/organization/organization-corp.controller.ts:30
      - insuniverse-server/apps/api/src/modules/client/client/client.controller.ts:69
      - insuniverse-server/apps/api/src/constants/api.constant.ts:13
  - id: response:envelope-shape
    status: active-prod
    evidence: [S-guard, A-comment]
    anchors:
      - insuniverse-server/apps/api/src/constants/api.constant.ts:2
      - insuniverse-server/apps/api/src/constants/client.constant.ts:2
      - insuniverse-server/apps/api/src/interceptors/api-response.interceptor.ts:12
      - insuniverse-server/apps/api/src/interceptors/api-response.interceptor.ts:23
      - insuniverse-server/apps/api/src/interceptors/client-response.interceptor.ts:24
  - id: response:error-passthrough
    status: active-prod
    evidence: [S-guard, A-comment]
    anchors:
      - insuniverse-server/apps/api/src/main.ts:22
      - insuniverse-server/apps/api/src/fliters/all-exception.filter.ts:32
      - insuniverse-server/apps/api/src/fliters/all-exception.filter.ts:35
      - insuniverse-server/apps/api/src/constants/api.constant.ts:100
      - insuniverse-server/apps/api/src/constants/api.constant.ts:101
  - id: response:validation-error-fields
    status: active-prod
    evidence: [S-contract, S-guard]
    anchors:
      - insuniverse-server/apps/api/src/app.module.ts:188
      - insuniverse-server/apps/api/src/pipes/validation.pipe.ts:48
      - insuniverse-server/apps/api/src/pipes/validation.pipe.ts:58
      - insuniverse-server/apps/api/src/constants/client.constant.ts:31
      - insuniverse-server/apps/api/src/constants/client.constant.ts:32
  - id: response:documented-shape-partial
    status: active-prod
    evidence: [S-contract, A-comment]
    limitations:
      - 선언 유무만 셌다. 선언된 형태가 실제 응답과 맞는지는 대조하지 않았다
    anchors:
      - insuniverse-server/apps/api/src/swagger.ts:12
      - insuniverse-server/apps/api/src/modules/admin/admin.controller.ts:36
      - insuniverse-server/apps/api/src/modules/advertisement/admin-advertisement.controller.ts:50
      - insuniverse-server/apps/api/src/modules/agency/organization/organization-corp.controller.ts:27
  - id: response:spec-surface-nonprod
    status: active-prod
    evidence: [S-contract, S-constant]
    anchors:
      - insuniverse-server/apps/api/src/main.ts:27
      - insuniverse-server/apps/api/src/swagger.ts:12
      - insuniverse-server/apps/api/src/swagger.ts:13
  - id: response:file-download-header
    status: active-prod
    evidence: [S-contract, S-constant]
    anchors:
      - insuniverse-server/apps/api/src/main.ts:53
      - insuniverse-server/apps/api/src/modules/agency/settlement/settlement-admin.controller.ts:146
      - insuniverse-server/apps/api/src/modules/analyze/analyze.service.ts:343
---

# 응답 계약 — 성공과 실패가 돌아오는 형태

이 문서는 **응답의 공통 형태**만 다룬다. 성공과 실패가 각각 어떤 겉모양으로 돌아오는가,
그 겉모양이 어디에 붙고 어디에 붙지 않는가가 여기다. 어떤 값이 요청으로 받아들여지는가는
입력 계약 문서(`01-input-validation.md`)가 든다.

개별 오류 코드의 뜻을 펼치는 목록은 여기 없다. 이 문서가 드는 것은 **필드 이름과 겉모양**이며,
코드의 의미 전개는 다른 축의 문서가 든다.

## 정책 (기획)

### response:no-global-envelope

응답을 공통 겉모양으로 감싸는 처리는 **전체에 걸려 있지 않다.** 그것을 따로 붙인 소수의
표면만 감싸인 형태로 나가고, 나머지는 처리 결과를 그대로 낸다.

따라서 "이 시스템의 응답은 항상 결과 코드와 본문으로 감싸여 있다" 는 서술은 사실과 다르다.
겉모양이 붙는지 여부는 표면마다 다르며, 감싸는 표면은 전체 중 아주 적다.

### response:envelope-shape

감싸는 겉모양은 **두 갈래**다. 하나는 결과 코드·안내 문구·본문 세 자리를 두고 본문을 한 단
아래에 넣는다. 다른 하나는 결과 코드·안내 문구를 본문과 **같은 높이**에 둔다 — 본문이 한 단
아래로 내려가지 않는다.

두 갈래는 쓰는 자리가 다르다. 앞쪽은 운영사 내부 화면이 부르는 표면, 뒤쪽은 고객사가 부르는
표면이다. 필드 이름도 서로 다르다.

### response:error-passthrough

실패 응답은 **감싸지 않는다.** 처리 중 올라온 예외가 들고 있는 본문이 그대로 나간다. 성공
응답에 겉모양을 붙인 표면이라도 실패할 때는 그 겉모양이 붙지 않는다.

예외로 표현되지 않은 오류는 서버 내부 오류 한 건으로 접히고, 원인은 기록으로만 남는다.
이용자에게는 같은 문구가 나간다.

### response:validation-error-fields

입력 검증에 실패했을 때 나가는 응답은 **어느 표면에서 나가든 같은 필드 이름**을 쓴다. 운영사
내부 화면이 부르는 표면도 고객사용 필드 이름으로 응답한다.

성공 응답의 필드 이름은 표면에 따라 갈리는데 입력 검증 실패 응답은 갈리지 않는다. 이 어긋남은
관측한 그대로이며 여기서 옳고 그름을 판정하지 않는다.

### response:documented-shape-partial

응답 형태를 **규격으로 선언한 경로가 선언하지 않은 경로보다 적다.** 선언이 붙은 자리에서만
규격 화면이 응답 모양을 보여주고, 나머지는 실제로 불러 보아야 알 수 있다.

선언이 붙은 자리에서도 선언된 것은 **본문의 모양**이다. 겉모양으로 감싸는 표면에서는 그
감싸개가 선언에 들어 있지 않으므로, 규격에 적힌 모양과 실제로 돌아오는 모양이 한 겹 어긋난다.

### response:spec-surface-nonprod

응답 규격을 사람이 열어보는 화면은 **운영 환경에서 붙지 않는다.** 운영이 아닌 환경에서만
띄워지며, 운영 환경에서 그 주소를 열면 화면이 없다.

따라서 운영에서 실제로 어떤 모양이 돌아오는지는 규격 화면으로 확인할 수 없다.

### response:file-download-header

파일을 내려받는 응답은 본문 대신 파일 자체를 보내며, 파일 이름을 담은 머리말을 함께 보낸다.
브라우저에서 부르는 경우 그 머리말을 읽을 수 있도록 교차 출처 설정에서 따로 열어 둔다. 열어
두지 않으면 파일은 받아지되 이름이 사라진다.

이 응답에는 결과 코드나 안내 문구가 없다. 겉모양 규칙이 적용되지 않는 유일한 응답 갈래다.

## 강제 지점 (개발)

### response:no-global-envelope

- 강제 방식: 컨트롤러 단위 인터셉터. 전역 등록 목록에 겉모양 인터셉터가 없다 — `insuniverse-server/apps/api/src/main.ts:25` 의 전역 인터셉터는 로깅용 하나뿐이다.
- 적용 지점(동결 기준점 실측 전수): `insuniverse-server/apps/api/src/modules/agency/organization/organization-admin.controller.ts:41` · `insuniverse-server/apps/api/src/modules/agency/organization/organization-corp.controller.ts:30` · `insuniverse-server/apps/api/src/modules/client/client/client.controller.ts:69`.
- 컨트롤러 총수: 114
- 봉투 적용 컨트롤러: 3
- 두 수는 `apps` 와 `libs` 아래 `*.controller.ts` 전수와, 그중 겉모양 인터셉터 두 종을 `@UseInterceptors` 로 다는 파일 수다.
- 전역으로 붙이지 않은 이유가 주석에 있다: `organization-admin.controller.ts:37` — 기존 컨트롤러들과의 충돌 방지.
- 성공 겉모양 상수: `insuniverse-server/apps/api/src/constants/api.constant.ts:13` — 결과 코드 `0000`.

### response:envelope-shape

- 강제 방식: 인터셉터 2종.
- 내부용 형태: `insuniverse-server/apps/api/src/interceptors/api-response.interceptor.ts:23` 이 성공 상수를 펼치고 본문을 `data` 키 아래에 넣는다. 필드 이름 계약은 `insuniverse-server/apps/api/src/constants/api.constant.ts:2` 의 `code` · `message` · `data`.
- 고객사용 형태: `insuniverse-server/apps/api/src/interceptors/client-response.interceptor.ts:24` 가 성공 상수와 본문을 **같은 높이로** 펼친다. 필드 이름 계약은 `insuniverse-server/apps/api/src/constants/client.constant.ts:2` 의 `resultCode` · `resultMsg`.
- 고객사용 성공 코드는 `S0000`, 내부용은 `0000` 이다.
- 두 인터셉터의 의도는 주석에 있다: `api-response.interceptor.ts:12`.

### response:error-passthrough

- 강제 방식: 전역 예외 필터. `insuniverse-server/apps/api/src/main.ts:22` 가 `AllExceptionFilter` 를 전역 등록한다.
- 통과: `insuniverse-server/apps/api/src/fliters/all-exception.filter.ts:32` — 예외가 들고 있는 응답 본문을 그대로 회신 본문으로 쓴다. 인터셉터는 성공 경로에만 걸리므로 이 경로에는 겉모양이 붙지 않는다.
- 접힘: `all-exception.filter.ts:35` — 예외가 아닌 오류는 서버 내부 오류 상수 한 건으로 대체하고 원인은 로그로만 남긴다.
- 그 상수: `insuniverse-server/apps/api/src/constants/api.constant.ts:101` — 내부용 코드는 `IE9999`. 고객사용 같은 뜻의 코드는 `insuniverse-server/apps/api/src/constants/client.constant.ts:16` 의 `E9999` 로 층마다 다르다.
- 두 층 공통이라는 표기가 주석에 있다: `api.constant.ts:100`.

### response:validation-error-fields

- 강제 방식: 전역 파이프의 예외 생성기. `insuniverse-server/apps/api/src/app.module.ts:188` 이 그 파이프를 전역 등록한다.
- 길이 오류 경로: `insuniverse-server/apps/api/src/pipes/validation.pipe.ts:48` 이 고객사용 상수를 복사해 안내 문구를 채운다. 상수는 `insuniverse-server/apps/api/src/constants/client.constant.ts:32` 이며 코드는 `E0004`.
- 그 밖의 입력 오류 경로: `validation.pipe.ts:58` — 상수는 `client.constant.ts:31` 이며 코드는 `E0003`.
- 두 상수 모두 `resultCode` · `resultMsg` 필드를 쓴다. 내부 표면의 성공 응답이 `code` · `message` 를 쓰는 것과 이 지점에서 갈린다.

### response:documented-shape-partial

- 강제 방식: 없음 — 이것은 강제 규칙이 아니라 **선언 커버리지 관측**이다. 강제하는 코드가 있으면 그 자리를 적었을 것이고 없으므로 없다고 적는다.
- 규격 문서 생성: `insuniverse-server/apps/api/src/swagger.ts:12`.
- 선언이 붙은 예: `insuniverse-server/apps/api/src/modules/admin/admin.controller.ts:36` · `insuniverse-server/apps/api/src/modules/advertisement/admin-advertisement.controller.ts:50`.
- 계수 규칙: `apps` 와 `libs` 아래 `*.controller.ts` 의 라우트 데코레이터 전수를 분모로, 응답 형태 선언 데코레이터를 분자로 센다. 동결 기준점에서 **선언한 쪽이 선언하지 않은 쪽보다 적다.** 두 수를 문면에 옮기지 않는 이유는 주석 마스킹 규칙에 따라 값이 갈리기 때문이다 — 규칙이 다르면 다른 수가 나오고, 문서에 박힌 수는 그 규칙을 들고 있지 않다.
- 선언과 실제가 한 겹 어긋나는 자리의 근거: `insuniverse-server/apps/api/src/modules/agency/organization/organization-corp.controller.ts:27` — 컨트롤러는 본문만 반환하고 겉모양은 인터셉터가 붙인다고 주석이 적는다. 응답 형태 선언은 그 본문 타입을 가리킨다.

### response:spec-surface-nonprod

- 강제 방식: 부팅 시점 분기. `insuniverse-server/apps/api/src/main.ts:27` — 실행 환경 이름이 운영이 **아닐 때만** 규격 화면을 붙인다.
- 붙는 것: `insuniverse-server/apps/api/src/swagger.ts:12` 가 규격 문서를 만들고 `:13` 이 그것을 한 경로에 건다.
- 되돌릴 지점이 없다 — 분기는 부팅 때 한 번 평가되고 실행 중에 바뀌지 않는다.

### response:file-download-header

- 강제 방식: 응답 머리말 선언 + 교차 출처 노출 목록.
- 선언 예(정적): `insuniverse-server/apps/api/src/modules/agency/settlement/settlement-admin.controller.ts:146` — 라우트에 머리말을 고정 선언한다.
- 선언 예(동적): `insuniverse-server/apps/api/src/modules/analyze/analyze.service.ts:343` — 파일 이름을 실행 중에 만들어 머리말에 넣는다.
- 노출: `insuniverse-server/apps/api/src/main.ts:53` — 교차 출처 설정의 노출 머리말 목록에 파일 이름 머리말이 들어 있다. 이 목록은 상수로 박혀 있다.

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

| 정책 id | 관리자 | 영업대리점 | 기업담당자 | 일반회원 | 기업회원 | server | site | admin |
|---|---|---|---|---|---|---|---|---|
| response:no-global-envelope | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| response:envelope-shape | 적용 | 미적용 | 적용 | 미적용 | 미적용 | 강제 지점 | — | 호출 경로 |
| response:error-passthrough | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| response:validation-error-fields | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | 호출 경로 | 호출 경로 |
| response:documented-shape-partial | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | — | — |
| response:spec-surface-nonprod | 적용 | 적용 | 적용 | 적용 | 적용 | 강제 지점 | — | — |
| response:file-download-header | 적용 | 미적용 | 적용 | 미적용 | 미적용 | 강제 지점 | — | 호출 경로 |

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

`response:envelope-shape` 가 좁은 것은 겉모양을 붙인 표면이 기업 조직 관리와 고객사 표면
셋뿐이기 때문이다. `response:documented-shape-partial` 과 `response:spec-surface-nonprod` 는
서버 쪽 표면 성질이라 화면 쪽 호출 경로가 없다.

## 예외·폴백

| 정책 id | 예외 조건 | 폴백 동작 | 확인 근거 |
|---|---|---|---|
| response:no-global-envelope | 컨트롤러에 겉모양 인터셉터가 걸려 있음 | 그 컨트롤러의 성공 응답만 감싸인다 | `organization-admin.controller.ts:41` · `client.controller.ts:69` |
| response:envelope-shape | 겉모양 인터셉터가 걸리지 않은 표면 | 겉모양 자체가 없다 | `main.ts:25` |
| response:envelope-shape | 본문이 비었거나 변환 결과가 없음 | 내부용은 본문 자리에 빈 값을, 고객사용은 본문 자리를 아예 두지 않는다 | `api-response.interceptor.ts:23` · `client-response.interceptor.ts:24` |
| response:error-passthrough | 예외로 표현되지 않은 오류 | 서버 내부 오류 상수 한 건으로 대체하고 원인은 로그에만 남긴다 | `all-exception.filter.ts:35` |
| response:validation-error-fields | 길이 상한 위반 | 길이 전용 상수를 쓰고 안내에 항목 이름과 제한값을 채운다 | `validation.pipe.ts:48` |
| response:validation-error-fields | 그 밖의 입력 오류 | 일반 입력 오류 상수를 쓴다 | `validation.pipe.ts:58` |
| response:documented-shape-partial | 라우트에 응답 형태 선언이 없음 | 규격 화면에 응답 모양이 나타나지 않는다 | `admin.controller.ts:36` |
| response:spec-surface-nonprod | 실행 환경 이름이 운영 | 규격 화면을 붙이지 않는다 | `main.ts:27` |
| response:file-download-header | 파일 응답이 아닌 경로 | 머리말을 붙이지 않는다 | `settlement-admin.controller.ts:146` |

## 연혁

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

| 릴리스 | 날짜 | SHA | 변경 요약 |
|---|---|---|---|
| — | 2024-08-27 | `a9e7f1e8` | 전역 예외 필터와 내부용 겉모양 인터셉터가 처음 생겼다 |
| — | 2024-12-01 | `5d7346d8` | 규격 화면을 운영 환경에서 빼는 분기가 지금 자리에 섰다 |
| — | 2024-12-03 | `e13f8395` | 서버 내부 오류 코드가 내부 표면 상수에 자리 잡았다 |
| — | 2025-03-17 | `bf64e84a` | 고객사용 겉모양과 실패 처리가 분리 신설되고 기존 것이 정리됐다 |
| — | 2025-03-17 | `7241229d` | 입력 검증 실패 응답이 고객사용 필드 이름을 쓰는 형태로 확정됐다 |
| — | 2025-04-10 | `915e7cc6` | 파일 이름 머리말이 교차 출처 노출 목록에 추가됐다 |
| — | 2026-05-20 | `62770c75` | 기업 조직 표면 둘이 겉모양을 컨트롤러 단위로 붙였다 — 전역 적용은 하지 않는다는 판단이 주석으로 남았다 |
