---
title: "Create Plan Product Mappings"
method: POST
path: "/v1/plans/{plan_id}/products"
tags: ["Product Catalog Plan Mapping"]
---

# Create Plan Product Mappings

`POST /v1/plans/{plan_id}/products`

## Request

- Base URL: `https://api.xdata.kr` (the document declares no server; this is the origin it was published from)
- URL: `https://api.xdata.kr/v1/plans/{plan_id}/products`
- Auth: one of:
  - HTTP basic
  - OAuth 2 (authorization code, authorization URL `https://api.xdata.kr/oauth/authorize`, token URL `https://api.xdata.kr/oauth/token`)

## Path parameters

- `plan_id` integer, required

## Query parameters

- `env_scope` 'production' | 'real_test' | 'sandbox', nullable: 환경을 고릅니다. 값은 production · real_test · sandbox 중 하나입니다. 경로 접두(/v1/sandbox/... · /v1/test/... · /v1/production/...) · X-Env-Scope 헤더 · env_scope 쿼리 가운데 한 통로로 반드시 보내야 합니다. 빼면 자동으로 채우지 않고 거절하며, 두 통로에 서로 다른 값을 담아도 거절합니다. 거절은 HTTP 422 이고 오류 코드는 VALIDATION_ENV_SCOPE_MISMATCH 입니다.

## Headers

- `Idempotency-Key` string, nullable
- `Authorization` string, nullable
- `X-Env-Scope` 'production' | 'real_test' | 'sandbox', nullable: 환경을 고릅니다. 값은 production · real_test · sandbox 중 하나입니다. 경로 접두(/v1/sandbox/... · /v1/test/... · /v1/production/...) · X-Env-Scope 헤더 · env_scope 쿼리 가운데 한 통로로 반드시 보내야 합니다. 빼면 자동으로 채우지 않고 거절하며, 두 통로에 서로 다른 값을 담아도 거절합니다. 거절은 HTTP 422 이고 오류 코드는 VALIDATION_ENV_SCOPE_MISMATCH 입니다.

## Request body

- PlanProductMappingCreateRequest: `POST /v1/plans/{plan_id}/products` 요청 본문. 옵션 A (단일): `product_id` + `product_version_id` + `is_default` 평면 본문. 옵션 B (다중): `mappings` 배열 nested 본문 (운영 효율). DTO `model_validator(mode='after')` 가 "양쪽 모두 지정" 또는 "양쪽 모두 미지정" 본문을 사전 거부한다 - 422 분기가 한 곳에 있어야 하므로 라우트 진입 전에 걸러진다. 라우트와 서비스는 옵션 A · B 분기와 DB 트랜잭션만 맡는다(빈 본문 · 모순 본문은 여기서 막혀 라우트에 닿지 않는다).
  - `product_id` integer, nullable: 옵션 A (단일 매핑). 옵션 B (mappings) 와 상호 배타. BIGINT 범위 잠금.
  - `product_version_id` integer, nullable: 옵션 A (단일 매핑). NULL=동적 (api_products.current_version_id 참조). BIGINT 범위 잠금.
  - `is_default` boolean, nullable: 옵션 A (단일 매핑). 기본 false (서비스 레벨 보강).
  - `mappings` PlanProductMappingItem[], nullable: 옵션 B (다중 매핑 일괄 추가/갱신). 1~100 행. 옵션 A (product_id) 와 상호 배타.
    - `product_id` integer, required: api_products.id 존재 + env_scope 일치 + status IN ('ACTIVE','DEPRECATED'). RETIRED/DRAFT 매핑 거부 → 409 CATALOG_RETIRED_PRODUCT_USE / CATALOG_VERSION_NOT_PUBLISHED. BIGINT 범위 잠금.
    - `product_version_id` integer, nullable: (선택) api_product_versions.id. NULL=동적 (api_products.current_version_id 참조). BIGINT 범위 잠금.
    - `is_default` boolean: true 지정 시 같은 plan 내 다른 default 매핑 자동 false 전환. 같은 plan 내 default 행 최대 1건(부분 유니크 인덱스가 강제한다).

## Response `201`

Successful Response

- PlanProductMappingResponse: `POST /v1/plans/{plan_id}/products` 응답 본문. - 신규 매핑 INSERT만 발생 시 → 201 Created (응답 상태 코드는 라우트 영역). - 기존 활성 매핑 본문 갱신 발생 시 → 200 OK + 기존 행 soft delete + 신규 INSERT (감사 이벤트 2건 = `plan_product.mapping.deleted` + `plan_product.mapping.created`). 응답 envelope: ```json { "data": { "plan_id": 100, "mappings": [ {...}, ... ] } } ``` 본 DTO 는 `data` 내부 본문 (`plan_id` + `mappings` 배열) 만 정의 — 외부 `data` 래핑은 통합 담당 envelope 영역.
  - `plan_id` integer, required: plans.id. 경로 파라미터 echo. BIGINT 범위 잠금.
  - `mappings` PlanProductMappingResponseItem[], required: 신규 INSERT 또는 갱신된 매핑 행 목록. 옵션 A 는 1행, 옵션 B 는 N 행.
    - `id` integer, required: plan_products.id.
    - `product_id` integer, required
    - `product_version_id` integer, nullable
    - `is_default` boolean, required
    - `env_scope` 'sandbox' | 'real_test' | 'production', required
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/xdata/apis/xboss-api.md) · [All operations](https://skmtc.dev/xdata/apis/xboss-api/llms.txt) · [OpenAPI document](https://skmtc.dev/xdata/apis/xboss-api/revisions/816de78950db?raw)
