fix(core,board,ecommerce,plugins): API 문서 미채움 마커 전수 채움 + 미해석 lang 키 정정 + IDV 조회 시도횟수 비노출

이슈 본연: API 레퍼런스의 미채움 마커 5종(실측 제외/TODO/필드 없음/대표 에러 없음)
1,194건을 코드에서 읽어 전수 채우고(0건), api:docgen 재생성이 사람이 채운 내용을
손실·열화시키던 멱등성 결함 4종(CRLF, 표 파이프 이스케이프, 에러 서술 보존, 중복
라우트명 키)을 근본 수정. --check 를 실측 제외와 드리프트가 구분되도록 재정의.
ParameterDescriber/ResourceFieldDescriber 에 leaf 폴백·SEO 스코프·공통 사전 확장.

파생 결함(문서 실측 중 발견): ResponseHelper 기본 메시지 키 6종이 존재하지 않는
messages.* 를 가리켜 응답 message 가 번역문 대신 키 문자열로 노출되던 문제를 common.*
으로 정정하고, 코어 7 + gdpr/marketing/pay_kginicis/verification/ecommerce 확장의
호출부와 누락 lang 키(게시판 10키 + 이커머스 category_images 4키 포함)를 ko/en/ja
전수 정정. 중복 그룹 키 2건은 기존 키로 호출부 통합.

보안: GET /api/identity/challenges/{id} 는 권한 가드 없는 공개 폴링 엔드포인트인데
Service::getStatus 가 attempts/max_attempts 를 응답에 담아 남은 시도 횟수를 추론할 수
있었다. 두 필드를 제거하고, 챌린지 화면(admin_basic/basic)의 서버 attempts 참조를
제거(남은 횟수 UI 는 query fallback + verify 실패 시 로컬 증가로 유지).

부수 정리(변경셋에 들어온 board 컨트롤러의 audit 사전 결함): FormRequest 4개 신설로
base Request 주입 제거, CommentController 의 Board 직접 호출을 BoardService 위임으로
전환, PHPDoc @return 보강. board 스위트에서 발견한 stale test 3건(seed SSoT 불일치,
admin 계정 user 역할 누락)도 같은 세션에서 정정.

버전 정렬(공개 release 기준): 코어 7.0.3→7.0.4, sirsoft-board 1.0.1→1.0.2,
verification_kginicis 1.0.0→1.0.1, admin_basic 1.0.1→1.0.4.
This commit is contained in:
HeuJung committed 2026-08-06 14:16:40 +09:00
1 parent b07d6c8953
commit 6c8381f164
155 files changed
+17192 -1829

No files matched your search

@@ -337,13 +337,47 @@ HTTP/1.1 200
**응답 예시**
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "메모를 조회했습니다.",
"data": {
"data": [
{
"id": 1,
"uuid": "9f1c0b4e-3a2d-4c8e-9f77-0a1b2c3d4e5f",
"title": "샘플 메모",
"content": "메모 본문 내용",
"created_at": "2026-04-21 09:00:00",
"updated_at": "2026-04-21 09:00:00",
"abilities": {
"can_create": false,
"can_update": false,
"can_delete": false
}
}
],
"meta": {
"current_page": 1,
"last_page": 3,
"per_page": 10,
"total": 21
},
"abilities": {
"can_create": false,
"can_update": false,
"can_delete": false
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 429 | Too Many Requests | `throttle:600,1` 초과 (분당 600회) |
| 500 | Internal Server Error | 목록 조회 중 예외 발생 시 `messages.memo.fetch_failed` (`메모 조회에 실패했습니다.`) |
<!-- @generated:end -->
@@ -382,7 +416,7 @@ HTTP/1.1 200
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| id | path | string | 예 | — | 대상 리소스의 식별자 |
| id | path | string | 예 | — | 조회할 메모의 고유번호(기본 키). `MemoService::getMemo($id)` 로 직접 조회하며 없으면 404 |
**요청 예시**
@@ -395,17 +429,47 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체(`MemoResource`)의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 메모 고유번호 (기본 키) |
| uuid | string | `9f1c0b4e-3a2d-4c8e-9f77-0a1b2c3d4e5f` | 메모 UUID (외부 식별자, 36자) |
| title | string | `샘플 메모` | 메모 제목 (최대 255자) |
| content | string | `메모 본문 내용` | 메모 본문 |
| created_at | string | `2026-04-21 09:00:00` | 생성 일시 (사용자 타임존 기준 `Y-m-d H:i:s`, 값이 없으면 필드 자체 생략) |
| updated_at | string | `2026-04-21 09:00:00` | 수정 일시 (사용자 타임존 기준 `Y-m-d H:i:s`, 값이 없으면 필드 자체 생략) |
| abilities | object | `{"can_create":false,"can_update":false,"can_delete":false}` | 권한 능력 (`gnuboard7-hello_module.memos.{create,update,delete}` 보유 여부). `MemoResource` 는 `ownerField()` 미정의 → `is_owner` 미포함 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "메모를 조회했습니다.",
"data": {
"id": 1,
"uuid": "9f1c0b4e-3a2d-4c8e-9f77-0a1b2c3d4e5f",
"title": "샘플 메모",
"content": "메모 본문 내용",
"created_at": "2026-04-21 09:00:00",
"updated_at": "2026-04-21 09:00:00",
"abilities": {
"can_create": false,
"can_update": false,
"can_delete": false
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 해당 `id` 의 메모가 없는 경우 — `ModelNotFoundException` → `메모를 찾을 수 없습니다.` |
| 429 | Too Many Requests | `throttle:600,1` 초과 (분당 600회) |
| 500 | Internal Server Error | 조회 중 예외 발생 시 `메모 조회에 실패했습니다.` |
<!-- @generated:end -->
@@ -125,11 +125,30 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `5` | 게시판 유형 ID (기본 키) |
| slug | string | `notice-board` | 유형 식별자 (basic, card, gallery 등) — 소문자로 시작하는 소문자/숫자/하이픈 조합 |
| name | object | `{"ko":"공지형","en":"Notice"}` | 유형명 (다국어: `{"ko": "기본형", "en": "Basic List"}`) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "게시판 유형이 생성되었습니다.",
"data": {
"id": 5,
"slug": "notice-board",
"name": {
"ko": "공지형",
"en": "Notice"
}
}
}
```
**에러 응답**
@@ -167,11 +186,17 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "게시판 유형이 삭제되었습니다.",
"data": null
}
```
**에러 응답**
@@ -179,7 +204,8 @@ Authorization: Bearer {YOUR_TOKEN}
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 해당 `id` 의 게시판 유형이 없는 경우 (`게시판 유형을 찾을 수 없습니다.`) |
| 422 | Unprocessable Entity | 해당 유형을 사용 중인 게시판이 있는 경우 (`:count개 게시판에서 사용 중인 유형은 삭제할 수 없습니다.`) 또는 게시판 기본 유형(`basic_defaults.type`)으로 설정된 경우 (`기본값으로 설정된 유형은 삭제할 수 없습니다.`) |
<!-- @generated:end -->
@@ -215,11 +241,30 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `2` | 게시판 유형 ID (기본 키) |
| slug | string | `basic` | 유형 식별자 (basic, card, gallery 등) — 수정되지 않음 |
| name | object | `{"ko":"기본형","en":"Basic List"}` | 유형명 (다국어: `{"ko": "기본형", "en": "Basic List"}`) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "게시판 유형이 수정되었습니다.",
"data": {
"id": 2,
"slug": "basic",
"name": {
"ko": "기본형",
"en": "Basic List"
}
}
}
```
**에러 응답**
+558 -36
View File
@@ -82,11 +82,46 @@ Content-Disposition: form-data; name="temp_key"
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드. FileUploader 컴포넌트 호환을 위해 파일 메타는 `data.data` 로 한 번 더 감싸집니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| data | object | `{"id":1,"hash":"apidocsmpl1", ...}` | 업로드된 첨부파일 메타 객체 (아래 하위 필드) |
| data.id | integer | `1` | 첨부파일 기본 키 (게시판별 첨부 테이블의 내부 식별자) |
| data.hash | string | `apidocsmpl1` | 첨부파일 해시 식별자 (다운로드/미리보기 URL 에 사용) |
| data.original_filename | string | `apidoc-sample.png` | 사용자가 업로드한 원본 파일명 |
| data.stored_filename | string | `—` | 스토리지에 실제 저장된 파일명 (충돌 방지용 내부 파일명) |
| data.mime_type | string | `image/png` | 업로드 파일의 MIME 타입 |
| data.size | integer | `2048` | 파일 크기 (바이트) |
| data.url | string | `/api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1` | 첨부파일 접근 URL (`AttachmentService::getUrl()` 산물) |
| data.order | integer | `0` | 첨부파일 표시 순서 (0 부터 오름차순) |
| data.created_at | string | `2026-07-08 10:41:34` | 업로드(생성) 일시 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "파일이 업로드되었습니다.",
"data": {
"data": {
"id": 1,
"hash": "apidocsmpl1",
"original_filename": "apidoc-sample.png",
"stored_filename": "apidocsmpl1.png",
"mime_type": "image/png",
"size": 2048,
"url": "/api/modules/sirsoft-board/boards/apidoc-sample-board/attachment/apidocsmpl1",
"order": 0,
"created_at": "2026-07-08 10:41:34"
}
}
}
```
**에러 응답**
@@ -94,8 +129,9 @@ Content-Disposition: form-data; name="temp_key"
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.attachments.upload`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 (슬러그에 해당하는 게시판 없음 — `게시판을 찾을 수 없습니다.`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 업로드 처리 실패 (`파일 업로드에 실패했습니다.`) |
<!-- @generated:end -->
@@ -128,11 +164,17 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 JSON 봉투를 반환하지 않습니다. 성공 시 파일 본문을 그대로 내려주는 `StreamedResponse`(바이너리 스트림)이며, `data` 구조가 없습니다. 실패 시에만 JSON 에러 봉투가 반환됩니다._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
Content-Type: image/png
Content-Disposition: attachment; filename="apidoc-sample.png"
(바이너리 파일 내용)
```
**에러 응답**
@@ -140,7 +182,8 @@ Authorization: Bearer {YOUR_TOKEN}
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.attachments.download`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 슬러그에 해당하는 게시판이 없거나(`게시판을 찾을 수 없습니다.`), 해시에 해당하는 첨부가 없거나(`파일을 찾을 수 없습니다.`), 실제 파일이 스토리지에 없는 경우 |
| 500 | Internal Server Error | 다운로드 처리 실패 (`파일 다운로드에 실패했습니다.`) |
<!-- @generated:end -->
@@ -180,11 +223,21 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "첨부파일 순서가 변경되었습니다.",
"data": null
}
```
**에러 응답**
@@ -192,8 +245,9 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.attachments.upload`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 슬러그에 해당하는 게시판이 없는 경우 (`게시판을 찾을 수 없습니다.`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 순서 변경 처리 실패 (`첨부파일 순서 변경에 실패했습니다.`) |
<!-- @generated:end -->
@@ -224,7 +278,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
@@ -264,11 +318,14 @@ HTTP/1.1 200
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) |
| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) |
| per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 |
| search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) |
**요청 예시**
```http
GET /api/modules/sirsoft-board/admin/board/{slug}/posts HTTP/1.1
GET /api/modules/sirsoft-board/admin/board/{slug}/posts?page=1&per_page=1&search=%EC%98%88%EC%8B%9C%EA%B0%92 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
@@ -412,6 +469,7 @@ HTTP/1.1 200
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
<!-- @generated:end -->
@@ -443,11 +501,120 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`PostResource` — 게시글 수정(PUT) 응답과 동일한 구조)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| category | null | `null` | 게시글 분류(카테고리) 문자열. 게시판이 카테고리를 쓰지 않거나 미지정 시 null (최대 50자). |
| author | object | `{"uuid":"a234c2b1-…","name":"API 문서 샘플 사용자"}` | 작성자 사용자 객체 (uuid/name — author 관계 파생) |
| is_notice | boolean | `false` | 공지 게시글 여부 |
| is_secret | boolean | `false` | 비밀글 여부 |
| content_mode | string | `html` | 본문 편집 모드. `html`(위지윅/HTML) 또는 `text`(평문). |
| is_new | boolean | `true` | 신규(NEW) 표시 대상 여부 (게시판 `new_display_hours` 기준) |
| status | string | `published` | 게시글 상태 코드. `published` / `blinded` / `deleted` 중 하나. |
| status_label | string | `게시됨` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) |
| view_count | integer | `0` | 조회수 (집계) |
| comment_count | integer | `0` | 댓글 수 (집계) |
| reply_count | integer | `0` | 답변글 수 (집계) |
| attachment_count | integer | `0` | 첨부파일 수 (집계) |
| has_attachment | boolean | `false` | 첨부파일 보유 여부 |
| thumbnail | null | `null` | 썸네일 이미지 URL/경로 (없으면 null) |
| parent_id | null | `null` | 원글 ID (답변글일 때만 값 존재) |
| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위) |
| is_reply | boolean | `false` | 답변글 여부 |
| created_at | string | `2026-07-08 10:41:34` | 생성 일시 |
| created_at_formatted | string | `방금 전` | `created_at` 값의 표시용 포맷 문자열 |
| is_author | boolean | `true` | 현재 사용자가 작성자인지 여부 |
| is_guest_post | boolean | `false` | 비회원 작성 게시글 여부 |
| title | string | `API 문서 샘플 게시글` | 제목 |
| content | string | `<p>API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.</p>` | 본문 내용 |
| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 작성자 사용자 식별자 |
| trigger_type | string | `user` | 상태 변경을 유발한 주체 (`report`/`admin`/`system`/`auto_hide`/`user`/`cascade`) |
| updated_at | string | `2026-07-08 10:41:34` | 최종 수정 일시 |
| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) |
| ip_address | string | `127.0.0.1` | 작성 요청이 발생한 IP 주소 |
| action_logs | array | `[]` | 블라인드/복원/삭제 처리 이력 목록. `admin.manage` 권한 보유자에게만 노출. |
| board | null | `null` | 소속 게시판 정보 객체. board 관계 미로드 시 null. |
| navigation | null | `null` | 이전/다음 게시글 이동 정보. 쓰기 응답에서는 계산하지 않아 null. |
| parent | null | `null` | 원글 객체 (parent 관계 파생) |
| comments | null | `null` | 댓글 목록. comments 관계 미로드 시 null. |
| attachments | null | `null` | 첨부파일 목록. attachments 관계 미로드 시 null. |
| replies | null | `null` | 답변글 목록. replies 관계 미로드 시 null. |
| is_already_reported | boolean | `false` | 현재 사용자가 이미 신고한 게시글인지 여부 |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_write":true,…}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "게시글이 등록되었습니다.",
"data": {
"id": 1,
"category": null,
"author": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"avatar": null,
"status": "active",
"status_label": "활성",
"is_guest": false
},
"is_notice": false,
"is_secret": false,
"content_mode": "html",
"is_new": true,
"status": "published",
"status_label": "게시됨",
"view_count": 0,
"comment_count": 0,
"reply_count": 0,
"attachment_count": 0,
"has_attachment": false,
"thumbnail": null,
"parent_id": null,
"depth": 0,
"is_reply": false,
"created_at": "2026-07-08 10:41:34",
"created_at_formatted": "방금 전",
"is_author": true,
"is_guest_post": false,
"title": "API 문서 샘플 게시글",
"content": "<p>API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.</p>",
"user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"trigger_type": "user",
"updated_at": "2026-07-08 10:41:34",
"deleted_at": null,
"ip_address": "127.0.0.1",
"action_logs": [],
"board": null,
"navigation": null,
"parent": null,
"comments": null,
"attachments": null,
"replies": null,
"is_already_reported": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_write": true,
"can_read_secret": true,
"can_read_comments": true,
"can_write_comments": true,
"can_upload": true,
"can_download": true,
"can_manage": true
}
}
}
```
**에러 응답**
@@ -456,7 +623,8 @@ Authorization: Bearer {YOUR_TOKEN}
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.write`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 제목/본문 길이, 금지 키워드, 작성 쿨다운, 원글(`parent_id`) 유효성 위반 포함 |
| 500 | Internal Server Error | 게시글 생성 처리 실패 (`게시글 등록에 실패했습니다.`) |
<!-- @generated:end -->
@@ -474,11 +642,13 @@ Authorization: Bearer {YOUR_TOKEN}
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) |
| post_id | query | integer | 아니오 | min 1 | post 식별자 |
| parent_id | query | integer | 아니오 | min 1 | parent 식별자 |
**요청 예시**
```http
GET /api/modules/sirsoft-board/admin/board/{slug}/posts/form-data HTTP/1.1
GET /api/modules/sirsoft-board/admin/board/{slug}/posts/form-data?post_id=1&parent_id=1 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
@@ -527,6 +697,7 @@ HTTP/1.1 200
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.write`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 제목/본문 길이, 금지 키워드, 작성 쿨다운, 원글(`parent_id`) 유효성 위반 포함 |
<!-- @generated:end -->
@@ -808,7 +979,7 @@ HTTP/1.1 200
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.write|sirsoft-board.{slug}.admin.manage`)이 없는 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.write\|sirsoft-board.{slug}.admin.manage`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
<!-- @generated:end -->
@@ -1425,11 +1596,127 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`PostResource` — 복원된 게시글. 게시글 수정(PUT) 응답과 동일한 구조이며, `status` 가 `published` 로 되돌아가고 `action_logs` 에 `restore` 이력이 1건 추가됩니다)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| category | null | `null` | 게시글 분류(카테고리) 문자열 (미지정 시 null) |
| author | object | `{"uuid":"a234c2b1-…","name":"API 문서 샘플 사용자"}` | 작성자 사용자 객체 (uuid/name — author 관계 파생) |
| is_notice | boolean | `false` | 공지 게시글 여부 |
| is_secret | boolean | `false` | 비밀글 여부 |
| content_mode | string | `html` | 본문 편집 모드 (`html` / `text`) |
| is_new | boolean | `true` | 신규(NEW) 표시 대상 여부 |
| status | string | `published` | 복원 후 게시글 상태 (`published` 로 되돌아감) |
| status_label | string | `게시됨` | 상태의 사람이 읽는 라벨 |
| view_count | integer | `44` | 조회수 (집계) |
| comment_count | integer | `0` | 댓글 수 (집계) |
| reply_count | integer | `0` | 답변글 수 (집계) |
| attachment_count | integer | `0` | 첨부파일 수 (집계) |
| has_attachment | boolean | `false` | 첨부파일 보유 여부 |
| thumbnail | null | `null` | 썸네일 이미지 URL/경로 |
| parent_id | null | `null` | 원글 ID (답변글일 때만 값 존재) |
| depth | integer | `0` | 계층 트리에서의 깊이 |
| is_reply | boolean | `false` | 답변글 여부 |
| created_at | string | `2026-07-08 10:41:34` | 생성 일시 |
| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 |
| is_author | boolean | `true` | 현재 사용자가 작성자인지 여부 |
| is_guest_post | boolean | `false` | 비회원 작성 게시글 여부 |
| title | string | `API 문서 샘플 게시글` | 제목 |
| content | string | `<p>API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.</p>` | 본문 내용 |
| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 작성자 사용자 식별자 |
| trigger_type | string | `admin` | 상태 변경을 유발한 주체 (복원은 관리자 직권이므로 `admin`) |
| updated_at | string | `2026-07-08 15:01:45` | 최종 수정 일시 |
| deleted_at | null | `null` | 소프트 삭제 일시 (복원 후 null) |
| ip_address | string | `127.0.0.1` | 작성 요청이 발생한 IP 주소 |
| action_logs | array | `[{"action":"restore","reason":null,"admin_name":"API 문서 샘플 사용자","created_at":"2026-07-08 06:01:45"}]` | 블라인드/복원/삭제 처리 이력 목록. `admin.manage` 권한 보유자에게만 노출. |
| board | null | `null` | 소속 게시판 정보 객체 (관계 미로드 시 null) |
| navigation | null | `null` | 이전/다음 게시글 이동 정보 (쓰기 응답은 미계산 → null) |
| parent | null | `null` | 원글 객체 (parent 관계 파생) |
| comments | null | `null` | 댓글 목록 (관계 미로드 시 null) |
| attachments | null | `null` | 첨부파일 목록 (관계 미로드 시 null) |
| replies | null | `null` | 답변글 목록 (관계 미로드 시 null) |
| is_already_reported | boolean | `false` | 현재 사용자가 이미 신고한 게시글인지 여부 |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 |
| abilities | object | `{"can_read":true,"can_write":true,…}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "게시글이 복원되었습니다.",
"data": {
"id": 1,
"category": null,
"author": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"avatar": null,
"status": "active",
"status_label": "활성",
"is_guest": false
},
"is_notice": false,
"is_secret": false,
"content_mode": "html",
"is_new": true,
"status": "published",
"status_label": "게시됨",
"view_count": 44,
"comment_count": 0,
"reply_count": 0,
"attachment_count": 0,
"has_attachment": false,
"thumbnail": null,
"parent_id": null,
"depth": 0,
"is_reply": false,
"created_at": "2026-07-08 10:41:34",
"created_at_formatted": "4시간 전",
"is_author": true,
"is_guest_post": false,
"title": "API 문서 샘플 게시글",
"content": "<p>API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.</p>",
"user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"trigger_type": "admin",
"updated_at": "2026-07-08 15:01:45",
"deleted_at": null,
"ip_address": "127.0.0.1",
"action_logs": [
{
"action": "restore",
"reason": null,
"admin_name": "API 문서 샘플 사용자",
"created_at": "2026-07-08 06:01:45"
}
],
"board": null,
"navigation": null,
"parent": null,
"comments": null,
"attachments": null,
"replies": null,
"is_already_reported": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_write": true,
"can_read_secret": true,
"can_read_comments": true,
"can_write_comments": true,
"can_upload": true,
"can_download": true,
"can_manage": true
}
}
}
```
**에러 응답**
@@ -1437,8 +1724,9 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.manage`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 404 | Not Found | 슬러그에 해당하는 게시판 또는 ID 에 해당하는 게시글이 없는 경우 (`존재하지 않는 게시글입니다.`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`reason` 이 1000자를 초과하는 등) |
| 500 | Internal Server Error | 복원 처리 실패 (`게시글 복원에 실패했습니다.`) |
<!-- @generated:end -->
@@ -1471,20 +1759,91 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`CommentResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 댓글 기본 키 (내부 식별자) |
| post_id | integer | `1` | 댓글이 속한 게시글 식별자 |
| parent_id | null | `null` | 상위 댓글 식별자 (대댓글일 때만 값 존재) |
| content | string | `API 문서 샘플 댓글입니다.` | 댓글 본문 내용 |
| author | object | `{"uuid":"a234c2b1-…","name":"API 문서 샘플 사용자"}` | 작성자 사용자 객체 (author 관계 파생) |
| is_secret | boolean | `false` | 비밀 댓글 여부 |
| status | string | `published` | 댓글 상태 코드 (`published` / `blinded` / `deleted`) |
| status_label | string | `게시됨` | 상태의 사람이 읽는 라벨 (상태 Enum label() 산물) |
| depth | integer | `0` | 댓글 계층 깊이 (0 = 최상위 댓글, 서비스에서 자동 계산) |
| replies_count | integer | `0` | 이 댓글에 달린 대댓글 수 (집계) |
| created_at | string | `2026-07-08 10:41:34` | 생성 일시 |
| created_at_formatted | string | `방금 전` | `created_at` 값의 표시용 포맷 문자열 |
| updated_at | string | `2026-07-08 10:41:34` | 최종 수정 일시 |
| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) |
| is_cascade_deleted | boolean | `false` | 상위 게시글/댓글 삭제로 연쇄 삭제된 댓글인지 여부 |
| ip_address | string | `127.0.0.1` | 작성 요청이 발생한 IP 주소 (권한자에게만 노출) |
| action_logs | array | `[]` | 블라인드/복원/삭제 처리 이력 목록. `admin.manage` 권한 보유자에게만 노출. |
| is_author | boolean | `true` | 현재 사용자가 작성자인지 여부 |
| is_guest_comment | boolean | `false` | 비회원 작성 댓글 여부 |
| is_already_reported | boolean | `false` | 현재 사용자가 이미 신고한 댓글인지 여부 |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) |
| abilities | object | `{"can_read":true,"can_write":true,"can_manage":true}` | 현재 사용자가 이 댓글에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "댓글이 등록되었습니다.",
"data": {
"id": 1,
"post_id": 1,
"parent_id": null,
"content": "API 문서 샘플 댓글입니다.",
"author": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"avatar": null,
"status": "active",
"status_label": "활성",
"is_guest": false
},
"is_secret": false,
"status": "published",
"status_label": "게시됨",
"depth": 0,
"replies_count": 0,
"created_at": "2026-07-08 10:41:34",
"created_at_formatted": "방금 전",
"updated_at": "2026-07-08 10:41:34",
"deleted_at": null,
"is_cascade_deleted": false,
"ip_address": "127.0.0.1",
"action_logs": [],
"is_author": true,
"is_guest_comment": false,
"is_already_reported": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_write": true,
"can_manage": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.comments.write`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.comments.write`)이 없거나, 게시판의 댓글 기능이 꺼진 경우 (`이 게시판은 댓글 기능이 비활성화되어 있습니다.`) |
| 404 | Not Found | 슬러그에 해당하는 게시판이 없는 경우 (`게시판을 찾을 수 없습니다.`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 댓글 길이, 금지 키워드, 작성 쿨다운, 대상 게시글·상위 댓글 유효성 위반 포함 |
| 500 | Internal Server Error | 댓글 생성 처리 실패 (`댓글 등록에 실패했습니다.`) |
<!-- @generated:end -->
@@ -1516,7 +1875,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
@@ -1537,8 +1896,9 @@ HTTP/1.1 200
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.comments.write|sirsoft-board.{slug}.admin.manage`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.comments.write\|sirsoft-board.{slug}.admin.manage`)이 없거나, 타인/비회원 댓글을 `admin.manage` 없이 삭제하려는 경우(`접근 권한이 없습니다.`), 게시판의 댓글 기능이 꺼진 경우(`이 게시판은 댓글 기능이 비활성화되어 있습니다.`) |
| 404 | Not Found | ID 에 해당하는 댓글 또는 슬러그에 해당하는 게시판이 없는 경우 (`댓글을 찾을 수 없습니다.`) |
| 500 | Internal Server Error | 삭제 처리 실패 (`댓글 삭제에 실패했습니다.`) |
<!-- @generated:end -->
@@ -1572,20 +1932,91 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`CommentResource` — 댓글 생성/블라인드 응답과 동일한 구조)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 댓글 기본 키 (내부 식별자) |
| post_id | integer | `1` | 댓글이 속한 게시글 식별자 |
| parent_id | null | `null` | 상위 댓글 식별자 (대댓글일 때만 값 존재) |
| content | string | `API 문서 샘플 댓글입니다. (수정)` | 수정된 댓글 본문 내용 |
| author | object | `{"uuid":"a234c2b1-…","name":"API 문서 샘플 사용자"}` | 작성자 사용자 객체 (author 관계 파생) |
| is_secret | boolean | `false` | 비밀 댓글 여부 |
| status | string | `published` | 댓글 상태 코드 (`published` / `blinded` / `deleted`) |
| status_label | string | `게시됨` | 상태의 사람이 읽는 라벨 |
| depth | integer | `0` | 댓글 계층 깊이 (0 = 최상위 댓글) |
| replies_count | integer | `0` | 이 댓글에 달린 대댓글 수 (집계) |
| created_at | string | `2026-07-08 10:41:34` | 생성 일시 |
| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 |
| updated_at | string | `2026-07-08 15:01:45` | 최종 수정 일시 (수정 시각으로 갱신) |
| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) |
| is_cascade_deleted | boolean | `false` | 상위 게시글/댓글 삭제로 연쇄 삭제된 댓글인지 여부 |
| ip_address | null | `null` | 작성 요청이 발생한 IP 주소 (권한자에게만 노출) |
| action_logs | array | `[]` | 블라인드/복원/삭제 처리 이력 목록. `admin.manage` 권한 보유자에게만 노출. |
| is_author | boolean | `true` | 현재 사용자가 작성자인지 여부 |
| is_guest_comment | boolean | `false` | 비회원 작성 댓글 여부 |
| is_already_reported | boolean | `false` | 현재 사용자가 이미 신고한 댓글인지 여부 |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 |
| abilities | object | `{"can_read":true,"can_write":true,"can_manage":true}` | 현재 사용자가 이 댓글에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "댓글이 수정되었습니다.",
"data": {
"id": 1,
"post_id": 1,
"parent_id": null,
"content": "API 문서 샘플 댓글입니다. (수정)",
"author": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"avatar": null,
"status": "active",
"status_label": "활성",
"is_guest": false
},
"is_secret": false,
"status": "published",
"status_label": "게시됨",
"depth": 0,
"replies_count": 0,
"created_at": "2026-07-08 10:41:34",
"created_at_formatted": "4시간 전",
"updated_at": "2026-07-08 15:01:45",
"deleted_at": null,
"is_cascade_deleted": false,
"ip_address": null,
"action_logs": [],
"is_author": true,
"is_guest_comment": false,
"is_already_reported": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_write": true,
"can_manage": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.comments.write`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.comments.write`)이 없거나, 타인/비회원 댓글을 `admin.manage` 없이 수정하려는 경우(`접근 권한이 없습니다.`), 게시판의 댓글 기능이 꺼진 경우(`이 게시판은 댓글 기능이 비활성화되어 있습니다.`) |
| 404 | Not Found | ID 에 해당하는 댓글 또는 슬러그에 해당하는 게시판이 없는 경우 (`댓글을 찾을 수 없습니다.`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 수정 처리 실패 (`댓글 수정에 실패했습니다.`) |
<!-- @generated:end -->
@@ -1733,6 +2164,7 @@ HTTP/1.1 200
| slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) |
| postId | path | string | 예 | — | 대상 post의 식별자 |
| id | path | string | 예 | — | 대상 리소스의 식별자 |
| reason | body | string | 아니오 | max 1000 | 댓글 블라인드 처리 사유(최대 1000자). 처리 이력에 기록되며, 미지정 시 빈 문자열로 저장됩니다. |
**요청 예시**
@@ -1741,23 +2173,113 @@ PATCH /api/modules/sirsoft-board/admin/board/{slug}/posts/{postId}/comments/{id}
Host: api.example.com
Accept: application/json
Authorization: Bearer {YOUR_TOKEN}
Content-Type: application/json
{
"reason": "예시값"
}
```
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`CommentResource` — 복원된 댓글. 블라인드 응답과 동일한 구조이며, `status` 가 `published` 로 되돌아가고 `action_logs` 에 `restore` 이력이 1건 추가됩니다)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 댓글 기본 키 (내부 식별자) |
| post_id | integer | `1` | 댓글이 속한 게시글 식별자 |
| parent_id | null | `null` | 상위 댓글 식별자 (대댓글일 때만 값 존재) |
| content | string | `API 문서 샘플 댓글입니다.` | 댓글 본문 내용 (복원되어 원문이 다시 노출됨) |
| author | object | `{"uuid":"a234c2b1-…","name":"API 문서 샘플 사용자"}` | 작성자 사용자 객체 (author 관계 파생) |
| is_secret | boolean | `false` | 비밀 댓글 여부 |
| status | string | `published` | 복원 후 댓글 상태 (`published` 로 되돌아감) |
| status_label | string | `게시됨` | 상태의 사람이 읽는 라벨 |
| depth | integer | `0` | 댓글 계층 깊이 (0 = 최상위 댓글) |
| replies_count | integer | `0` | 이 댓글에 달린 대댓글 수 (집계) |
| created_at | string | `2026-07-08 10:41:34` | 생성 일시 |
| created_at_formatted | string | `4시간 전` | `created_at` 값의 표시용 포맷 문자열 |
| updated_at | string | `2026-07-08 15:01:45` | 최종 수정 일시 |
| deleted_at | null | `null` | 소프트 삭제 일시 (미삭제 시 null) |
| is_cascade_deleted | boolean | `false` | 상위 게시글/댓글 삭제로 연쇄 삭제된 댓글인지 여부 |
| ip_address | null | `null` | 작성 요청이 발생한 IP 주소 (권한자에게만 노출) |
| action_logs | array | `[{"action":"blind","reason":"실측 예시값",…},{"action":"restore","reason":null,…}]` | 블라인드/복원/삭제 처리 이력 목록 (복원 이력이 추가됨). `admin.manage` 권한 보유자에게만 노출. |
| is_author | boolean | `true` | 현재 사용자가 작성자인지 여부 |
| is_guest_comment | boolean | `false` | 비회원 작성 댓글 여부 |
| is_already_reported | boolean | `false` | 현재 사용자가 이미 신고한 댓글인지 여부 |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 |
| abilities | object | `{"can_read":true,"can_write":true,"can_manage":true}` | 현재 사용자가 이 댓글에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "댓글이 복원되었습니다.",
"data": {
"id": 1,
"post_id": 1,
"parent_id": null,
"content": "API 문서 샘플 댓글입니다.",
"author": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"avatar": null,
"status": "active",
"status_label": "활성",
"is_guest": false
},
"is_secret": false,
"status": "published",
"status_label": "게시됨",
"depth": 0,
"replies_count": 0,
"created_at": "2026-07-08 10:41:34",
"created_at_formatted": "4시간 전",
"updated_at": "2026-07-08 15:01:45",
"deleted_at": null,
"is_cascade_deleted": false,
"ip_address": null,
"action_logs": [
{
"action": "blind",
"reason": "실측 예시값",
"admin_name": "API 문서 샘플 사용자",
"created_at": "2026-07-08 06:01:44"
},
{
"action": "restore",
"reason": null,
"admin_name": "API 문서 샘플 사용자",
"created_at": "2026-07-08 06:01:45"
}
],
"is_author": true,
"is_guest_comment": false,
"is_already_reported": false,
"is_owner": true,
"abilities": {
"can_read": true,
"can_write": true,
"can_manage": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.manage`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.manage`)이 없거나, 게시판의 댓글 기능이 꺼진 경우 (`이 게시판은 댓글 기능이 비활성화되어 있습니다.`) |
| 404 | Not Found | ID 에 해당하는 댓글 또는 슬러그에 해당하는 게시판이 없는 경우 (`댓글을 찾을 수 없습니다.`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 복원 처리 실패 (`댓글 복원에 실패했습니다.`) |
<!-- @generated:end -->
File diff suppressed because it is too large. Load diff
@@ -217,11 +217,31 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| affected_count | integer | `3` | 실제로 상태가 변경된 신고 건수 (영구삭제·동일 상태 등 전환 불가 건은 제외) |
| restored_count | integer | `1` | 상태 변경(반려 등)에 따라 공개 상태로 복구된 대상 콘텐츠 건수 |
| manual_blind_restored | integer | `1` | 그중 관리자 수동 블라인드(`admin` 트리거)였다가 함께 복구된 건수 |
| status_label | string | `반려` | 전환된 신고 상태의 사람이 읽는 라벨 (`ReportStatus::label()` 산물) |
| message | string | `3개 신고를 반려(으)로 변경했습니다.` | 화면에 그대로 노출 가능한 안내 문구 (봉투 최상위 `message` 와 동일 값) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "3개 신고를 반려(으)로 변경했습니다. 수동 블라인드 처리된 1건이 함께 복구되었습니다.",
"data": {
"affected_count": 3,
"restored_count": 1,
"manual_blind_restored": 1,
"status_label": "반려",
"message": "3개 신고를 반려(으)로 변경했습니다. 수동 블라인드 처리된 1건이 함께 복구되었습니다."
}
}
```
**에러 응답**
@@ -230,6 +250,7 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.manage`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 일괄 상태 변경 처리 중 오류가 발생한 경우 (`sirsoft-board::messages.reports.bulk_status_update_failed`) |
<!-- @generated:end -->
@@ -268,11 +289,57 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| status_counts | object | `{"pending":{"count":2,"label":"접수"},"deleted":{"count":1,"label":"영구삭제"}}` | 선택된 신고들의 현재 상태별 집계 맵. 키는 상태 코드(pending/review/rejected/suspended/deleted), 값은 `{count, label}` |
| changeable_status_counts | object | `{"pending":{"count":2,"label":"접수"}}` | `status_counts` 에서 영구삭제(deleted)와 전환 대상 상태(`target_status`)를 제외한 집계 (실제로 변경될 항목만) |
| total_count | integer | `3` | 요청한 `ids` 의 총 개수 |
| changeable_count | integer | `2` | 실제로 상태 변경이 가능한 건수 (`total_count` − `unchangeable_count`) |
| deleted_count | integer | `1` | 선택 항목 중 영구삭제(deleted) 상태로 변경 불가한 건수 |
| same_status_count | integer | `0` | 선택 항목 중 이미 `target_status` 와 동일해 변경이 무의미한 건수 (`target_status` 가 deleted 이면 0) |
| unchangeable_count | integer | `1` | 변경 불가 총 건수 (`deleted_count` + `same_status_count`) |
| all_deleted | boolean | `false` | 선택 항목이 모두 영구삭제 상태인지 여부 |
| all_unchangeable | boolean | `false` | 선택 항목 전부가 변경 불가인지 여부 (`changeable_count === 0`) |
| target_status_label | string \| null | `반려` | `target_status` 의 사람이 읽는 라벨. `target_status` 미지정 시 null |
| restorable_blind_count | integer | `1` | `target_status` 가 `rejected` 일 때, 반려 처리로 공개 상태로 복구될 블라인드 대상 건수 (그 외 상태에서는 0) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "상태별 건수를 조회했습니다.",
"data": {
"status_counts": {
"pending": {
"count": 2,
"label": "접수"
},
"deleted": {
"count": 1,
"label": "영구삭제"
}
},
"changeable_status_counts": {
"pending": {
"count": 2,
"label": "접수"
}
},
"total_count": 3,
"changeable_count": 2,
"deleted_count": 1,
"same_status_count": 0,
"unchangeable_count": 1,
"all_deleted": false,
"all_unchangeable": false,
"target_status_label": "반려",
"restorable_blind_count": 1
}
}
```
**에러 응답**
@@ -281,6 +348,7 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.view`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 상태별 건수 집계 중 오류가 발생한 경우 (`sirsoft-board::messages.reports.status_counts_failed`) |
<!-- @generated:end -->
@@ -310,7 +378,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
@@ -573,20 +641,109 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ReportResource` — 상태 변경 후 재조회한 신고 케이스)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| board_id | integer \| null | `1` | 게시판 ID (게시판 삭제 시 null) |
| board | object | `{"id":1,"name":"공지사항","slug":"notice","title":"...","current_status":"blinded","deleted_at":null}` | 신고 대상이 속한 게시판 정보 + 대상 제목/현재 상태. 게시판 삭제 시 첫 신고 로그의 스냅샷 값으로 폴백합니다. |
| target_type | string | `post` | 신고 대상 타입 (`post`, `comment`) |
| target_type_label | string | `게시글` | `target_type` 값의 사람이 읽는 라벨 |
| target_id | integer | `1` | 신고 대상 ID (동적 테이블의 ID) |
| post_id | integer \| null | `1` | 대상 게시글 ID. 대상이 댓글이면 상위 게시글 ID |
| content | null | `null` | 본문 내용 (상세(`show`) 요청에서만 채워지며, 상태 변경 응답에서는 null) |
| content_mode | string | `html` | 대상 본문의 형식 모드 (`text`/`html`). 스냅샷이 없으면 `text` |
| content_preview | string | `API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.` | 대상 본문의 미리보기 (앞 100자 발췌) |
| author | object | `{"uuid":"...","name":"홍길동","email":"user@example.com","is_guest":false}` | 신고 대상 콘텐츠의 작성자. 회원 정보가 없으면 스냅샷 이름 + `is_guest: true` |
| reporter | object \| null | `{"uuid":"...","name":"신고자","email":"reporter@example.com","is_guest":false}` | 대표 신고자(첫 번째 신고 로그) 정보. 신고 로그가 없으면 null |
| reason_type | string \| null | `abuse` | 대표 신고 사유 코드 (첫 신고 로그의 사유 Enum 값) |
| reason_type_label | string \| null | `욕설/비방` | `reason_type` 의 사람이 읽는 라벨 |
| status | string | `rejected` | 변경된 신고 상태 (`pending`, `review`, `rejected`, `suspended`, `deleted`) |
| status_label | string | `반려` | 상태의 사람이 읽는 라벨 (`ReportStatus::label()`) |
| status_variant | string | `info` | 상태 배지 색상/스타일 변형 키 (`ReportStatus::variant()`) |
| processor | object \| null | `{"uuid":"...","name":"관리자"}` | 상태를 변경한 처리 관리자 (uuid/name). 미지정 시 null |
| processed_at | string \| null | `2026-07-08 10:41:34` | 처리 일시 |
| metadata | null | `null` | 메타데이터 (상세(`show`) 요청에서만 채워지며, 상태 변경 응답에서는 null) |
| report_count | integer \| null | `null` | 케이스 신고 건수 집계. 목록(`paginateGrouped`) 응답에서만 채워지며 단건 재조회에서는 null |
| last_reported_at | string \| null | `2026-07-08 10:41:34` | 마지막 신고 일시 (재접수 시 갱신) |
| is_reactivated | boolean | `false` | 반려/중단 후 재접수된 케이스인지 여부 (`last_activated_at` 존재 여부) |
| target_status | null | `null` | 대상 콘텐츠의 현재 상태. 목록 조회의 서브쿼리로만 주입되며 단건 재조회에서는 null |
| target_trigger_type | null | `null` | 대상 상태를 유발한 트리거 유형. 목록 조회에서만 채워지며 단건 재조회에서는 null |
| target_status_label | null | `null` | `target_status` 의 라벨. `target_status` 가 없으면 null |
| created_at | string | `2026-07-08 10:41:34` | 생성 일시 |
| updated_at | string | `2026-07-08 10:42:10` | 최종 수정 일시 |
| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자(`reporter_id`)인지 여부 |
| abilities | object | `{"can_view":true,"can_manage":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "신고 상태가 변경되었습니다.",
"data": {
"id": 1,
"board_id": 1,
"board": {
"id": 1,
"name": "API 문서 샘플 게시판",
"slug": "apidoc-sample-board",
"title": "API 문서 샘플 게시글",
"current_status": "published",
"deleted_at": null
},
"target_type": "post",
"target_type_label": "게시글",
"target_id": 1,
"post_id": 1,
"content": null,
"content_mode": "html",
"content_preview": "<p>API 레퍼런스 실측용 완전 샘플 게시글 본문입니다.</p>",
"author": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com",
"is_guest": false
},
"reporter": null,
"reason_type": null,
"reason_type_label": null,
"status": "rejected",
"status_label": "반려",
"status_variant": "info",
"processor": {
"uuid": "a1e0a91a-fba6-491c-a53e-7285a5686857",
"name": "관리자"
},
"processed_at": "2026-07-08 10:42:10",
"metadata": null,
"report_count": null,
"last_reported_at": "2026-07-08 10:41:34",
"is_reactivated": false,
"target_status": null,
"target_trigger_type": null,
"target_status_label": null,
"created_at": "2026-07-08 10:41:34",
"updated_at": "2026-07-08 10:42:10",
"is_owner": false,
"abilities": {
"can_view": true,
"can_manage": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.manage`)이 없는 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.reports.manage`)이 없거나, 권한 스코프(본인만/역할 범위)를 벗어난 신고에 접근한 경우 (`auth.scope_denied`) |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 영구삭제(`deleted`) 상태 신고의 상태를 변경하려 한 경우에도 422 (`sirsoft-board::messages.reports.cannot_change_deleted_status`) |
| 500 | Internal Server Error | 상태 변경 처리 중 오류가 발생한 경우 (`sirsoft-board::messages.reports.status_update_failed`) |
<!-- @generated:end -->
@@ -363,36 +363,36 @@ HTTP/1.1 200
| notifications | body | array | 아니오 | — | 알림 채널 설정. `channels` 배열의 각 항목에 채널 식별자(id), 활성화 여부(is_active), 정렬 순서(sort_order)를 담아 저장합니다. |
| basic_defaults | body | array | 아니오 | — | 기본 설정 카테고리 값. 게시판 타입·페이지당 글 수·정렬·댓글/답글·길이 제한·파일 업로드·기본 권한 등 basic_defaults 하위 키를 저장합니다. `basic_defaults.allowed_extensions` 는 `basic_defaults.use_file_upload` 가 `true` 일 때만 최소 1개가 필수이며, `false`/`null` 이면 검증에서 제외되어 빈 배열도 허용됩니다. `min_title_length`·`min_comment_length`·`new_display_hours` 의 하한은 `config('sirsoft-board.limits')` 선언을 따르며 `0` 을 허용합니다. |
| report_policy | body | array | 아니오 | — | 신고 정책 카테고리 값. 자동 숨김 임계치/대상, 일일 신고 한도, 거부 누적 제한, 관리자·작성자 신고 알림 설정을 저장합니다. |
| report_policy.auto_hide_threshold | body | integer | 아니오 | min 0, max 100 | <!-- TODO: 용도 --> |
| report_policy.auto_hide_target | body | string | 아니오 | `post`, `comment`, `both` | <!-- TODO: 용도 --> |
| report_policy.daily_report_limit | body | integer | 아니오 | min 0, max 100 | <!-- TODO: 용도 --> |
| report_policy.rejection_limit_count | body | integer | 아니오 | min 0, max 50 | <!-- TODO: 용도 --> |
| report_policy.rejection_limit_days | body | integer | 아니오 | min 1, max 365 | <!-- TODO: 용도 --> |
| report_policy.notify_admin_on_report | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| report_policy.notify_admin_on_report_scope | body | string | 아니오 | `per_case`, `per_report` | <!-- TODO: 용도 --> |
| report_policy.notify_admin_on_report_channels | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| report_policy.notify_author_on_report_action | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| report_policy.notify_author_on_report_action_channels | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| report_policy.auto_hide_threshold | body | integer | 아니오 | min 0, max 100 | 자동 숨김 신고 수 (이 횟수 이상 신고되면 자동 숨김 처리) |
| report_policy.auto_hide_target | body | string | 아니오 | `post`, `comment`, `both` | 자동 숨김 대상 (게시글 / 댓글 / 둘 다) |
| report_policy.daily_report_limit | body | integer | 아니오 | min 0, max 100 | 사용자별 일일 신고 한도 |
| report_policy.rejection_limit_count | body | integer | 아니오 | min 0, max 50 | 신고 기각 한도 (누적 기각 횟수 제한) |
| report_policy.rejection_limit_days | body | integer | 아니오 | min 1, max 365 | 신고 기각 한도 산정 기간(일) |
| report_policy.notify_admin_on_report | body | boolean | 아니오 | — | 신고 접수 시 관리자 알림 발송 여부 |
| report_policy.notify_admin_on_report_scope | body | string | 아니오 | `per_case`, `per_report` | 관리자 신고 알림 발송 단위 (신고 대상 건별 / 개별 신고마다) |
| report_policy.notify_admin_on_report_channels | body | array | 아니오 | — | 관리자 신고 알림 발송 채널 목록 |
| report_policy.notify_author_on_report_action | body | boolean | 아니오 | — | 신고 처리 결과를 작성자에게 알릴지 여부 |
| report_policy.notify_author_on_report_action_channels | body | array | 아니오 | — | 작성자 신고 처리 알림 발송 채널 목록 |
| report_permissions | body | array | 아니오 | — | 신고 관리 권한 역할. `view_roles`(조회 역할)와 `manage_roles`(관리 역할)의 역할 식별자 배열이며, 포함 시 설정 저장과 별개로 DB 권한 역할이 동기화됩니다. |
| report_permissions.view_roles | body | array | 아니오 | min 1 | <!-- TODO: 용도 --> |
| report_permissions.manage_roles | body | array | 아니오 | min 1 | <!-- TODO: 용도 --> |
| report_permissions.view_roles | body | array | 아니오 | min 1 | 신고 조회 권한 역할 목록 (`report_permissions` 포함 시 필수, 최소 1개) |
| report_permissions.manage_roles | body | array | 아니오 | min 1 | 신고 처리 권한 역할 목록 (`report_permissions` 포함 시 필수, 최소 1개) |
| display | body | array | 아니오 | — | 표시 설정 카테고리 값. 날짜 표시 형식(date_display_format: standard/relative) 등을 저장합니다. |
| display.date_display_format | body | string | 아니오 | `standard`, `relative` | <!-- TODO: 용도 --> |
| display.date_display_format | body | string | 아니오 | `standard`, `relative` | 날짜 표시 형식 (standard: 절대 표기 / relative: "3분 전" 형태의 상대 표기) |
| spam_security | body | array | 아니오 | — | 스팸·보안 카테고리 값. 글·댓글·신고 작성 쿨다운 시간(초)과 조회수 캐시 TTL을 저장합니다. |
| spam_security.post_cooldown_seconds | body | integer | 아니오 | min 0, max 3600 | <!-- TODO: 용도 --> |
| spam_security.comment_cooldown_seconds | body | integer | 아니오 | min 0, max 3600 | <!-- TODO: 용도 --> |
| spam_security.report_cooldown_seconds | body | integer | 아니오 | min 0, max 3600 | <!-- TODO: 용도 --> |
| spam_security.view_count_cache_ttl | body | integer | 아니오 | min 60, max 604800 | <!-- TODO: 용도 --> |
| spam_security.post_cooldown_seconds | body | integer | 아니오 | min 0, max 3600 | 게시글 작성 쿨다운(초). 도배 방지를 위한 연속 작성 대기 시간 (0이면 제한 없음) |
| spam_security.comment_cooldown_seconds | body | integer | 아니오 | min 0, max 3600 | 댓글 작성 쿨다운(초) |
| spam_security.report_cooldown_seconds | body | integer | 아니오 | min 0, max 3600 | 신고 쿨다운(초) |
| spam_security.view_count_cache_ttl | body | integer | 아니오 | min 60, max 604800 | 조회수 캐시 유효시간(초). 동일 사용자의 중복 조회수 증가를 막는 기간 |
| seo | body | array | 아니오 | — | SEO 설정 카테고리 값. 게시판 목록/개별/글 상세 페이지의 메타 제목·설명 템플릿과 각 페이지 SEO 활성화 여부를 저장합니다. |
| seo.meta_boards_title | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo.meta_boards_description | body | string | 아니오 | max 1000 | <!-- TODO: 용도 --> |
| seo.meta_board_title | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo.meta_board_description | body | string | 아니오 | max 1000 | <!-- TODO: 용도 --> |
| seo.meta_post_title | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo.meta_post_description | body | string | 아니오 | max 1000 | <!-- TODO: 용도 --> |
| seo.seo_boards | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.seo_board | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.seo_post_detail | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.meta_boards_title | body | string | 아니오 | max 500 | 게시판 목록 페이지의 메타 제목 템플릿 (예: `{site_name}`) |
| seo.meta_boards_description | body | string | 아니오 | max 1000 | 게시판 목록 페이지의 메타 설명 템플릿 |
| seo.meta_board_title | body | string | 아니오 | max 500 | 개별 게시판 페이지의 메타 제목 템플릿 (예: `{board_name}`) |
| seo.meta_board_description | body | string | 아니오 | max 1000 | 개별 게시판 페이지의 메타 설명 템플릿 (예: `{board_description}`) |
| seo.meta_post_title | body | string | 아니오 | max 500 | 게시글 상세 페이지의 메타 제목 템플릿 (예: `{board_name} - {post_title}`) |
| seo.meta_post_description | body | string | 아니오 | max 1000 | 게시글 상세 페이지의 메타 설명 템플릿 |
| seo.seo_boards | body | boolean | 아니오 | — | 게시판 목록 페이지의 SEO 페이지 생성 활성화 여부 |
| seo.seo_board | body | boolean | 아니오 | — | 개별 게시판 페이지의 SEO 페이지 생성 활성화 여부 |
| seo.seo_post_detail | body | boolean | 아니오 | — | 게시글 상세 페이지의 SEO 페이지 생성 활성화 여부 |
**요청 예시**
@@ -816,11 +816,62 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드. 성공(전체 적용 완료) 시와 중단(전체 롤백) 시의 필드 구성이 다릅니다 — 두 경우 모두 HTTP 200 입니다._
성공 시 (`rolled_back: false`)
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `3` | 실제로 설정이 적용된 게시판 수 (`BoardService::bulkApplySettings()` 반환값) |
| rolled_back | boolean | `false` | 롤백 발생 여부. 성공 시 항상 `false` |
중단·롤백 시 (`rolled_back: true`, `BulkApplyAbortedException`)
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| rolled_back | boolean | `true` | 적용 도중 실패해 전체 변경이 롤백되었음을 나타냅니다 |
| board | object\|null | `{"board_id":12,"slug":"notice","name":"공지사항"}` | 실패한 게시판 정보(`board_id`, `slug`, `name`). 컬럼 일괄 업데이트(단일 쿼리) 실패 시 게시판 특정이 불가하므로 `null` |
| board.board_id | integer | `12` | 실패한 게시판 ID |
| board.slug | string | `notice` | 실패한 게시판 슬러그 |
| board.name | string | `공지사항` | 실패한 게시판의 현재 로케일 이름 |
| failed_at | integer\|null | `2` | 실패 순번(1-base). 컬럼 업데이트 실패 시 `null` |
| total | integer | `5` | 일괄 적용 대상 게시판 총 수 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
성공 시
```json
{
"success": true,
"message": "sirsoft-board::messages.settings.bulk_apply_success",
"data": {
"updated_count": 3,
"rolled_back": false
}
}
```
중단·롤백 시 (HTTP 200)
```json
{
"success": true,
"message": "sirsoft-board::messages.settings.bulk_apply_aborted",
"data": {
"rolled_back": true,
"board": {
"board_id": 12,
"slug": "notice",
"name": "공지사항"
},
"failed_at": 2,
"total": 5
}
}
```
> `message` 는 컨트롤러가 넘기는 다국어 키(`sirsoft-board::messages.settings.*`)이며, 현재 모듈 `lang/ko/messages.php` 에 `settings` 그룹이 정의되어 있지 않아 키 문자열이 그대로 노출됩니다(다른 엔드포인트 실측 응답과 동일).
**에러 응답**
@@ -828,7 +879,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `fields` 누락/빈 배열, 허용되지 않은 필드명, `apply_all=false` 인데 `board_ids` 미지정, 존재하지 않는 `board_ids` 항목 등 |
| 500 | Internal Server Error | 일괄 적용 처리 중 롤백 예외 외의 오류가 발생한 경우 (`messages.settings.bulk_apply_failed`) |
<!-- @generated:end -->
@@ -856,11 +908,23 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| cleared | boolean | `true` | 캐시 초기화 수행 여부. 성공 시 항상 `true` (ModuleSettings 캐시 + 게시판 캐시 전체 초기화) |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "sirsoft-board::messages.settings.clear_cache_success",
"data": {
"cleared": true
}
}
```
**에러 응답**
@@ -868,6 +932,7 @@ Authorization: Bearer {YOUR_TOKEN}
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.update`)이 없는 경우 |
| 500 | Internal Server Error | 캐시 초기화 중 오류가 발생한 경우 (`messages.settings.clear_cache_error`) |
<!-- @generated:end -->
@@ -897,11 +962,34 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| category | string | `spam_security` | 요청 경로에서 전달된 카테고리명 (그대로 반환) |
| settings | object | `{"post_cooldown_seconds":0,"comment_cooldown_seconds":0,"report_cooldown_seconds":60,"view_count_cache_ttl":86400}` | 해당 카테고리의 설정값 맵 (기본값 + 저장값 병합 결과). 정의되지 않은 카테고리명이면 빈 객체 `{}` |
| abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (`can_update` — `sirsoft-board.settings.update` 권한 보유 여부) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "sirsoft-board::messages.settings.fetch_success",
"data": {
"category": "spam_security",
"settings": {
"post_cooldown_seconds": 0,
"comment_cooldown_seconds": 0,
"report_cooldown_seconds": 60,
"view_count_cache_ttl": 86400
},
"abilities": {
"can_update": true
}
}
}
```
**에러 응답**
@@ -910,6 +998,7 @@ Authorization: Bearer {YOUR_TOKEN}
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-board.settings.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 500 | Internal Server Error | 설정 조회 중 오류가 발생한 경우 (`messages.settings.fetch_failed`) |
<!-- @generated:end -->
@@ -5,12 +5,14 @@ namespace Modules\Sirsoft\Board\Http\Controllers\Admin;
use App\Http\Controllers\Api\Base\AdminBaseController;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Modules\Sirsoft\Board\Enums\PostStatus;
use Modules\Sirsoft\Board\Exceptions\AttachmentLimitExceededException;
use Modules\Sirsoft\Board\Exceptions\BoardNotFoundException;
use Modules\Sirsoft\Board\Exceptions\PostNotFoundException;
use Modules\Sirsoft\Board\Http\Requests\Admin\PostFormDataRequest;
use Modules\Sirsoft\Board\Http\Requests\Admin\PostFormMetaRequest;
use Modules\Sirsoft\Board\Http\Requests\Admin\PostIndexRequest;
use Modules\Sirsoft\Board\Http\Requests\BlindPostRequest;
use Modules\Sirsoft\Board\Http\Requests\RestorePostRequest;
use Modules\Sirsoft\Board\Http\Requests\StorePostRequest;
@@ -51,12 +53,11 @@ class PostController extends AdminBaseController
/**
* 게시글 목록을 조회합니다.
*
* @param Request $request HTTP 요청
* @param PostIndexRequest $request 검증된 목록 조회 요청
* @param string $slug 게시판 슬러그
* @return JsonResponse 게시글 목록 응답
*/
// audit:allow controller-base-request-injection reason: GET 목록 조회. all()로 필터·페이징 파라미터만 읽음 (검증 불필요)
public function index(Request $request, string $slug): JsonResponse
public function index(PostIndexRequest $request, string $slug): JsonResponse
{
// 권한은 라우트 미들웨어에서 체크됨 (sirsoft-board.{slug}.admin.posts.read)
try {
@@ -392,12 +393,11 @@ class PostController extends AdminBaseController
* - 수정 모드: 기존 게시글 데이터
* - 답변글 모드: 기본값이 설정된 폼 데이터
*
* @param Request $request HTTP 요청
* @param PostFormDataRequest $request 검증된 폼 데이터 요청
* @param string $slug 게시판 슬러그
* @return JsonResponse 폼 입력 데이터 응답
*/
// audit:allow controller-base-request-injection reason: GET 수정 폼 데이터 조회. 경로 파라미터(slug) + 쿼리만 read-only 참조 (검증 불필요)
public function getFormData(Request $request, string $slug): JsonResponse
public function getFormData(PostFormDataRequest $request, string $slug): JsonResponse
{
// 권한은 라우트 미들웨어에서 체크됨 (sirsoft-board.{slug}.admin.posts.write)
try {
@@ -480,12 +480,11 @@ class PostController extends AdminBaseController
*
* 게시판 정보, 원글 정보, 작성자 정보 등 화면 표시에 필요한 데이터를 반환합니다.
*
* @param Request $request HTTP 요청
* @param PostFormMetaRequest $request 검증된 폼 메타 요청
* @param string $slug 게시판 슬러그
* @return JsonResponse 폼 메타 데이터 응답
*/
// audit:allow controller-base-request-injection reason: GET 작성/수정 폼 메타 조회. 경로 파라미터(slug) + post_id 쿼리만 read-only 참조 (검증 불필요)
public function getFormMeta(Request $request, string $slug): JsonResponse
public function getFormMeta(PostFormMetaRequest $request, string $slug): JsonResponse
{
// 권한은 라우트 미들웨어에서 체크됨 (sirsoft-board.{slug}.admin.posts.write)
try {
@@ -0,0 +1,37 @@
<?php
namespace Modules\Sirsoft\Board\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
/**
* 관리자 게시글 폼 데이터 조회 요청 폼 검증
*
* 생성/수정/답글 모드를 구분하기 위해 post_id 또는 parent_id 를 선택적으로 받습니다.
* 권한은 라우트 permission 미들웨어가 담당합니다.
*/
class PostFormDataRequest extends FormRequest
{
/**
* 사용자가 이 요청을 수행할 권한이 있는지 확인합니다.
*
* @return bool 항상 true (권한은 미들웨어 체인에서 처리)
*/
public function authorize(): bool
{
return true;
}
/**
* 요청에 적용할 검증 규칙을 반환합니다.
*
* @return array<string, array<int, mixed>> 검증 규칙
*/
public function rules(): array
{
return [
'post_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
'parent_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
];
}
}
@@ -0,0 +1,34 @@
<?php
namespace Modules\Sirsoft\Board\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
/**
* 관리자 게시글 폼 메타데이터 조회 요청 폼 검증
*
* 별도 입력 파라미터를 받지 않습니다. 권한은 라우트 permission 미들웨어가 담당하며,
* 컨트롤러가 base Illuminate\Http\Request 를 주입받지 않도록 전용 FormRequest 를 둡니다.
*/
class PostFormMetaRequest extends FormRequest
{
/**
* 사용자가 이 요청을 수행할 권한이 있는지 확인합니다.
*
* @return bool 항상 true (권한은 미들웨어 체인에서 처리)
*/
public function authorize(): bool
{
return true;
}
/**
* 요청에 적용할 검증 규칙을 반환합니다.
*
* @return array<string, array<int, mixed>> 검증 규칙 (요청 파라미터 없음)
*/
public function rules(): array
{
return [];
}
}
@@ -0,0 +1,38 @@
<?php
namespace Modules\Sirsoft\Board\Http\Requests\Admin;
use Illuminate\Foundation\Http\FormRequest;
/**
* 관리자 게시글 목록 조회 요청 폼 검증
*
* 목록 필터·정렬·페이지네이션 파라미터를 받습니다. 값은 Service 의 buildListParams 에서
* 정규화되므로 여기서는 형식 수준만 검증합니다. 권한은 라우트 permission 미들웨어가 담당합니다.
*/
class PostIndexRequest extends FormRequest
{
/**
* 사용자가 이 요청을 수행할 권한이 있는지 확인합니다.
*
* @return bool 항상 true (권한은 미들웨어 체인에서 처리)
*/
public function authorize(): bool
{
return true;
}
/**
* 요청에 적용할 검증 규칙을 반환합니다.
*
* @return array<string, array<int, mixed>> 검증 규칙
*/
public function rules(): array
{
return [
'page' => ['sometimes', 'integer', 'min:1'],
'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'],
'search' => ['sometimes', 'nullable', 'string', 'max:255'],
];
}
}
@@ -175,6 +175,7 @@
- 쿠폰 사용조건의 상품 검색에서 결과가 많을 때 목록 끝까지 스크롤해도 다음 묶음이 더 불러와지지 않던 문제를 수정했습니다.
- 관리자가 주문상품을 일괄 구매확정할 때와 구매자가 직접 구매확정할 때, 구매확정 시점이 최초 확정 시각으로 동일하게 보존됩니다 — 이미 확정된 주문을 다시 확정 처리해도 시점이 덮어써지지 않습니다.
- 구매자 주문 상세의 결제 정보 영역에 결제 플러그인이 제공하는 공급가액·부가세·영수증 링크·에스크로 구매확정 버튼이 표시되지 않던 문제를 수정했습니다.
- 관리자가 카테고리 이미지를 등록·삭제하거나 순서를 바꾼 뒤 표시되는 안내 문구가 번역된 문장 대신 내부 식별자가 그대로 보이던 문제를 수정했습니다. 이제 사용 중인 언어에 맞는 안내 문구가 표시됩니다.
## [1.0.4] - 2026-07-16
@@ -225,7 +225,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만). 컨트롤러가 `ResponseHelper::success('sirsoft-ecommerce::messages.address.deleted')` 를 데이터 인수 없이 호출하므로 `data` 는 `null` 입니다._
**응답 예시**
@@ -387,7 +387,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만). 컨트롤러가 `ResponseHelper::success('sirsoft-ecommerce::messages.address.default_set')` 를 데이터 인수 없이 호출하므로 `data` 는 `null` 입니다. 변경된 목록은 배송지 목록 조회(`GET /user/addresses`)로 재조회합니다._
**응답 예시**
@@ -192,11 +192,60 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`BrandResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 생성된 브랜드의 기본 키 (내부 식별자) |
| name | object | `{"ko":"API 문서 샘플 브랜드","en":"API Doc Sample Brand"}` | 브랜드 이름 (로케일별 값 객체) |
| localized_name | string | `API 문서 샘플 브랜드` | `name` 의 현재 로케일 해석 값 (표시용 문자열) |
| slug | string | `apidoc-sample-brand` | URL 친화 식별자 (slug) |
| url | string | `apidoc-sample-brand` | SortableMenuItem 표시용 URL (slug 값을 그대로 노출) |
| website | string \| null | `null` | 브랜드 공식 웹사이트 URL (미입력 시 null) |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| is_active | boolean | `true` | 활성 여부 |
| icon | string | `tag` | SortableMenuList 컴포넌트용 아이콘 식별자 (고정값 `tag`) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 (사용자 타임존 기준 문자열) |
| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 (사용자 타임존 기준 문자열) |
| products_count | integer | `0` | 브랜드에 연결된 상품 수 (생성 직후이므로 0) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
> 생성 직후에는 `creator`/`updater` 관계가 로드되지 않으므로(`whenLoaded`) 응답에서 생략됩니다.
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "브랜드가 등록되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "API 문서 샘플 브랜드",
"en": "API Doc Sample Brand"
},
"localized_name": "API 문서 샘플 브랜드",
"slug": "apidoc-sample-brand",
"url": "apidoc-sample-brand",
"website": null,
"sort_order": 0,
"is_active": true,
"icon": "tag",
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 10:44:49",
"products_count": 0,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -294,11 +343,60 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`BrandResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 수정된 브랜드의 기본 키 (내부 식별자) |
| name | object | `{"ko":"API 문서 샘플 브랜드","en":"API Doc Sample Brand"}` | 브랜드 이름 (로케일별 값 객체) |
| localized_name | string | `API 문서 샘플 브랜드` | `name` 의 현재 로케일 해석 값 (표시용 문자열) |
| slug | string | `apidoc-sample-brand` | URL 친화 식별자 (slug) |
| url | string | `apidoc-sample-brand` | SortableMenuItem 표시용 URL (slug 값을 그대로 노출) |
| website | string \| null | `https://www.asus.com` | 브랜드 공식 웹사이트 URL (미입력 시 null) |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| is_active | boolean | `true` | 활성 여부 |
| icon | string | `tag` | SortableMenuList 컴포넌트용 아이콘 식별자 (고정값 `tag`) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 (사용자 타임존 기준 문자열) |
| updated_at | string | `2026-07-08 15:00:16` | 최종 수정 일시 (이번 수정 시각으로 갱신) |
| products_count | integer | `0` | 브랜드에 연결된 상품 수 |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
> 수정 직후에는 `creator`/`updater` 관계가 로드되지 않으므로(`whenLoaded`) 응답에서 생략됩니다.
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "브랜드가 수정되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "API 문서 샘플 브랜드",
"en": "API Doc Sample Brand"
},
"localized_name": "API 문서 샘플 브랜드",
"slug": "apidoc-sample-brand",
"url": "apidoc-sample-brand",
"website": "https://www.asus.com",
"sort_order": 0,
"is_active": true,
"icon": "tag",
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 15:00:16",
"products_count": 0,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -42,17 +42,34 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted_count | integer | `2` | 실제로 삭제된 장바구니 아이템 건수 (`CartService::deleteItems()` 반환값. 성공 메시지의 `:deleted_count` 치환값과 동일) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "2개 상품이 삭제되었습니다.",
"data": {
"deleted_count": 2
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`ids` 누락/빈 배열, 존재하지 않는 장바구니 ID — `error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 삭제 처리 중 예외 발생 (`messages.cart.delete_failed`) |
<!-- @generated:end -->
@@ -216,17 +233,104 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| items | array | `[{"id":962,"product_id":201,"quantity":1, …}]` | 이번 요청으로 담긴 장바구니 아이템 목록 (CartItemResource — 아래 "장바구니 아이템 필드" 참조) |
| cart_count | integer | `3` | 담기 후 장바구니에 들어있는 전체 아이템 수 (`CartService::bulkAddToCart()` 반환값) |
**장바구니 아이템 필드** (`items[]` — CartItemResource)
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `962` | 장바구니 아이템 ID (수량/옵션 변경·삭제 시 path 파라미터) |
| product_id | integer | `201` | 담긴 상품 ID |
| product_option_id | integer | `1086` | 선택된 상품 옵션 ID |
| quantity | integer | `1` | 구매 수량 |
| additional_options | array | `[]` | 선택된 추가옵션 해석 결과 (항목별 `additional_option_id`, `value_id`, `group_name`, `name`, `price_adjustment`, `price_adjustment_formatted`, `custom_text`) |
| additional_options_total | integer | `0` | 추가옵션 단위 추가금 합계 (수량 미곱셈) |
| created_at | string | `2026-07-14 10:00:00` | 담은 일시 (사용자 타임존 포맷) |
| updated_at | string | `2026-07-14 10:00:00` | 최종 수정 일시 (사용자 타임존 포맷) |
| product | object\|null | `{"id":201,"name":"티셔츠", …}` | 상품 요약 (`id`, `name`, `product_code`, `thumbnail_url`, `sales_status`, `display_status`) |
| product_option | object\|null | `{"id":1086,"selling_price":19000, …}` | 옵션 요약 (`id`, `option_code`, `option_name`, `option_name_localized`, `option_values`, `option_values_localized`, `list_price`, `list_price_formatted`, `multi_currency_list_price`, `selling_price`, `selling_price_formatted`, `multi_currency_selling_price`, `discount_rate`, `stock_quantity`, `is_active`) |
| available | boolean | `true` | 현재 구매 가능 여부 (`Product::isPurchasable()`) |
| unavailable_reason | string\|null | `sold_out` | 구매 불가 사유 (판매상태 `suspended`/`sold_out`/`coming_soon` 또는 전시상태 `hidden`. 구매 가능 시 `null`) |
| is_shippable_to_selected_country | boolean | `true` | 선택된 배송 국가로 배송 가능한지 (배송정책 기준) |
| selected_shipping_country | string | `KR` | 판정에 사용된 배송 국가 코드 |
| subtotal | integer | `19000` | 표시 소계 = (옵션 판매가 + 추가옵션 단위 합계) × 수량 (옵션이 로드된 경우에만 존재) |
| subtotal_formatted | string | `19,000원` | 소계 통화 포맷 문자열 |
| multi_currency_subtotal | object | `{"KRW":19000}` | 통화별 환산 소계 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "장바구니에 상품이 추가되었습니다.",
"data": {
"items": [
{
"id": 962,
"product_id": 201,
"product_option_id": 1086,
"quantity": 1,
"additional_options": [],
"additional_options_total": 0,
"created_at": "2026-07-14 10:00:00",
"updated_at": "2026-07-14 10:00:00",
"product": {
"id": 201,
"name": "티셔츠",
"product_code": "P-201",
"thumbnail_url": null,
"sales_status": "on_sale",
"display_status": "visible"
},
"product_option": {
"id": 1086,
"option_code": "OPT-1086",
"option_name": "색상/사이즈",
"option_name_localized": "색상/사이즈",
"option_values": ["BLACK", "L"],
"option_values_localized": ["블랙", "L"],
"list_price": 25000,
"list_price_formatted": "25,000원",
"multi_currency_list_price": {"KRW": 25000},
"selling_price": 19000,
"selling_price_formatted": "19,000원",
"multi_currency_selling_price": {"KRW": 19000},
"discount_rate": 24.0,
"stock_quantity": 50,
"is_active": true
},
"available": true,
"unavailable_reason": null,
"is_shippable_to_selected_country": true,
"selected_shipping_country": "KR",
"subtotal": 19000,
"subtotal_formatted": "19,000원",
"multi_currency_subtotal": {"KRW": 19000}
}
],
"cart_count": 3
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 400 | Bad Request | 비회원인데 `X-Cart-Key` 헤더가 없거나 형식(`ck_`+32자 영숫자)이 아닌 경우 |
| 403 | Forbidden | 타인 소유 장바구니 항목 조작 (`CartOperationException: access_denied`) |
| 404 | Not Found | 대상 장바구니 항목 없음 (`CartOperationException: item_not_found`) |
| 422 | Unprocessable Entity | 요청 파라미터 검증 실패 (`error.errors`) 또는 재고 부족·판매 중지·구매 대상 제한·구매 수량 한도 위반 (`CartUnavailableException` — `error` 에 `code: cart_unavailable`, `unavailable_items`, `has_stock_issue`, `has_status_issue`, `has_restriction_issue`, `has_min_qty_issue`, `has_max_qty_issue`), 옵션 없음/타상품 옵션 (`CartOperationException: option_not_found`/`invalid_option`) |
| 500 | Internal Server Error | 담기 처리 중 예외 발생 (`messages.cart.add_failed`) |
<!-- @generated:end -->
@@ -278,7 +382,10 @@ HTTP/1.1 200
**에러 응답**
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 422 | Unprocessable Entity | 비회원인데 `X-Cart-Key` 헤더가 없거나 형식(`ck_`+32자 영숫자)이 아닌 경우 (`CartKeyRequest` 검증) |
| 500 | Internal Server Error | 삭제 처리 중 예외 발생 (`messages.cart.delete_failed`) |
<!-- @generated:end -->
@@ -388,7 +495,9 @@ HTTP/1.1 200
**에러 응답**
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 500 | Internal Server Error | 키 발급 처리 중 예외 발생 (`messages.cart.key_issue_failed` — "장바구니 키 발급에 실패했습니다.") |
<!-- @generated:end -->
@@ -416,18 +525,36 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| merged_count | integer | `2` | 비회원 장바구니에서 회원 계정으로 옮겨진 아이템 건수 (`CartService::mergeGuestCartToUser()` 반환값) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": ":count개 상품이 장바구니로 병합되었습니다.",
"data": {
"merged_count": 2
}
}
```
> `messages.cart.merged` 는 `:count` 플레이스홀더를 갖지만 컨트롤러가 messageParams 를 전달하지 않으므로 응답 메시지에는 치환되지 않은 원문이 그대로 실립니다. 화면 표기는 `data.merged_count` 를 사용하십시오.
**에러 응답**
| 상태 | 조건 | 응답 |
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 422 | 미로그인 상태이거나 `X-Cart-Key` 헤더가 없음/형식 불일치 | `errors.auth` 또는 `errors.cart_key` |
| 500 | 병합 처리 실패 | `messages.cart.merge_failed` |
| 422 | Unprocessable Entity | 비로그인 상태로 호출(`auth` 오류 — "로그인이 필요합니다."), `X-Cart-Key` 헤더 누락 또는 형식(`ck_`+32자 영숫자) 불일치 (`MergeGuestCartRequest` 검증) |
| 500 | Internal Server Error | 병합 처리 중 예외 발생 (`messages.cart.merge_failed`) |
<!-- @generated:end -->
@@ -587,17 +714,30 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "장바구니에서 상품이 삭제되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 403 | Forbidden | 타인 소유 장바구니 항목 삭제 시도 (`CartOperationException: access_denied`) |
| 404 | Not Found | `id` 에 해당하는 장바구니 항목이 없는 경우 (`CartOperationException: item_not_found`) |
| 422 | Unprocessable Entity | 비회원인데 `X-Cart-Key` 헤더가 없거나 형식(`ck_`+32자 영숫자)이 아닌 경우 (`DeleteCartItemRequest` 검증) |
| 500 | Internal Server Error | 삭제 처리 중 예외 발생 (`messages.cart.delete_failed`) |
<!-- @generated:end -->
@@ -641,18 +781,77 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| item | object | `{"id":962,"product_option_id":1090, …}` | 옵션·수량이 반영된 장바구니 아이템 1건 (CartItemResource — 필드 구성은 POST `/cart` 의 "장바구니 아이템 필드" 표와 동일) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "옵션이 변경되었습니다.",
"data": {
"item": {
"id": 962,
"product_id": 201,
"product_option_id": 1090,
"quantity": 2,
"additional_options": [],
"additional_options_total": 0,
"created_at": "2026-07-14 10:00:00",
"updated_at": "2026-07-14 10:12:00",
"product": {
"id": 201,
"name": "티셔츠",
"product_code": "P-201",
"thumbnail_url": null,
"sales_status": "on_sale",
"display_status": "visible"
},
"product_option": {
"id": 1090,
"option_code": "OPT-1090",
"option_name": "색상/사이즈",
"option_name_localized": "색상/사이즈",
"option_values": ["WHITE", "M"],
"option_values_localized": ["화이트", "M"],
"list_price": 25000,
"list_price_formatted": "25,000원",
"multi_currency_list_price": {"KRW": 25000},
"selling_price": 19000,
"selling_price_formatted": "19,000원",
"multi_currency_selling_price": {"KRW": 19000},
"discount_rate": 24.0,
"stock_quantity": 40,
"is_active": true
},
"available": true,
"unavailable_reason": null,
"is_shippable_to_selected_country": true,
"selected_shipping_country": "KR",
"subtotal": 38000,
"subtotal_formatted": "38,000원",
"multi_currency_subtotal": {"KRW": 38000}
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 403 | Forbidden | 타인 소유 장바구니 항목 조작 (`CartOperationException: access_denied`) |
| 404 | Not Found | `id` 에 해당하는 장바구니 항목이 없는 경우 (`CartOperationException: item_not_found`) |
| 422 | Unprocessable Entity | 요청 파라미터 검증 실패(`error.errors`), 옵션 없음/타상품 옵션 지정 (`CartOperationException: option_not_found`/`invalid_option`), 재고 부족·판매 중지·구매 대상 제한·구매 수량 한도 위반 (`CartUnavailableException` — `error.code = cart_unavailable`, `unavailable_items`, `has_stock_issue`, `has_status_issue`, `has_restriction_issue`, `has_min_qty_issue`, `has_max_qty_issue`) |
| 500 | Internal Server Error | 옵션 변경 처리 중 예외 발생 (`messages.cart.update_failed`) |
<!-- @generated:end -->
@@ -690,18 +889,107 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| items | array | `[{"id":962,"quantity":2, …}]` | 수량 반영 후의 장바구니 전체 아이템 목록 (CartItemResource — 필드 구성은 POST `/cart` 의 "장바구니 아이템 필드" 표와 동일. 프론트가 refetch 없이 화면을 갱신하도록 전체를 함께 반환) |
| item_count | integer | `1` | 장바구니 아이템 개수 |
| calculation | object | `{"items":[…],"summary":{…},"promotions":{…},"validation_errors":[]}` | 선택 아이템 기준 금액 계산 결과 (GET `/cart` 의 `calculation` 과 동일 구조 — 소계·할인·배송비·적립·최종 결제금액 등) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "수량이 변경되었습니다.",
"data": {
"items": [
{
"id": 962,
"product_id": 201,
"product_option_id": 1086,
"quantity": 2,
"additional_options": [],
"additional_options_total": 0,
"created_at": "2026-07-14 10:00:00",
"updated_at": "2026-07-14 10:15:00",
"product": {
"id": 201,
"name": "티셔츠",
"product_code": "P-201",
"thumbnail_url": null,
"sales_status": "on_sale",
"display_status": "visible"
},
"product_option": {
"id": 1086,
"option_code": "OPT-1086",
"option_name": "색상/사이즈",
"option_name_localized": "색상/사이즈",
"option_values": ["BLACK", "L"],
"option_values_localized": ["블랙", "L"],
"list_price": 25000,
"list_price_formatted": "25,000원",
"multi_currency_list_price": {"KRW": 25000},
"selling_price": 19000,
"selling_price_formatted": "19,000원",
"multi_currency_selling_price": {"KRW": 19000},
"discount_rate": 24.0,
"stock_quantity": 50,
"is_active": true
},
"available": true,
"unavailable_reason": null,
"is_shippable_to_selected_country": true,
"selected_shipping_country": "KR",
"subtotal": 38000,
"subtotal_formatted": "38,000원",
"multi_currency_subtotal": {"KRW": 38000}
}
],
"item_count": 1,
"calculation": {
"items": [],
"summary": {
"subtotal": 38000,
"subtotal_formatted": "38,000원",
"total_discount": 0,
"discount_formatted": "0원",
"total_shipping_fee": 0,
"shipping_fee_formatted": "0원",
"payment_amount": 38000,
"payment_amount_formatted": "38,000원",
"final_amount": 38000,
"final_amount_formatted": "38,000원"
},
"promotions": {
"coupon_issue_ids": [],
"item_coupons": [],
"discount_code": null,
"product_promotions": {"coupons": [], "discount_codes": [], "events": []},
"order_promotions": {"coupons": [], "discount_codes": [], "events": []}
},
"validation_errors": []
}
}
}
```
> `calculation.summary` 는 GET `/cart` 응답과 동일한 전체 키 집합(`product_coupon_discount`, `code_discount`, `taxable_amount`, `points_earning` 등)을 포함합니다. 위 예시는 대표 키만 발췌한 것입니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 403 | Forbidden | 타인 소유 장바구니 항목 조작 (`CartOperationException: access_denied`) |
| 404 | Not Found | `id` 에 해당하는 장바구니 항목이 없는 경우 (`CartOperationException: item_not_found`) |
| 422 | Unprocessable Entity | 요청 파라미터 검증 실패(`quantity` 누락/범위 초과 — `error.errors`) 또는 재고 부족·판매 중지·구매 수량 한도 위반 (`CartUnavailableException` — `error.code = cart_unavailable`, `unavailable_items`, `has_stock_issue`, `has_status_issue`, `has_restriction_issue`, `has_min_qty_issue`, `has_max_qty_issue`) |
| 500 | Internal Server Error | 수량 변경 처리 중 예외 발생 (`messages.cart.update_failed`) |
<!-- @generated:end -->
@@ -218,11 +218,72 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`CategoryResource`). 생성 직후 `fresh(['images'])` 로 재조회하므로 `images` 관계만 포함되고 `parent`/`children` 키는 나타나지 않습니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| name | object | `{"ko":"의류","en":"Clothing"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
| description | object\|null | `null` | 설명 (다국어 필드는 로케일별 값 객체, 미설정 시 null) |
| localized_name | string | `의류` | `name` 의 현재 로케일 해석 값 |
| parent_id | integer\|null | `null` | 상위 카테고리 ID (최상위면 null) |
| path | string | `1` | 조상부터 자기 자신까지의 ID를 `/`로 이은 materialized path |
| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위) |
| sort_order | integer | `0` | 표시 정렬 순서 값 (미지정 시 형제 마지막 순번 자동 부여) |
| is_active | boolean | `true` | 활성 여부 |
| slug | string | `clothing` | URL 친화 식별자 (slug) |
| url | string | `clothing` | SortableMenuItem 표시용 URL (slug 값을 그대로 사용) |
| icon | string | `folder` | SortableMenuList 표시용 아이콘 (리소스에서 고정값 `folder`) |
| meta_title | string\|null | `null` | SEO 메타 제목 (미설정 시 null) |
| meta_description | string\|null | `null` | SEO 메타 설명 (미설정 시 null) |
| created_at | string | `2026-07-08 01:44:49` | 생성 일시 (`Y-m-d H:i:s`) |
| updated_at | string | `2026-07-08 01:44:49` | 최종 수정 일시 (`Y-m-d H:i:s`) |
| images | array | `[]` | 카테고리 이미지 배열 (각 항목: id/hash/original_filename/mime_type/size/size_formatted/download_url/order/is_image/alt_text). `temp_key` 로 연결된 임시 이미지가 여기에 포함됨 |
| products_count | integer | `0` | 연결된 상품 개수 (집계) |
| children_count | integer | `0` | 하위 카테고리 개수 (집계) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "카테고리가 등록되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "API 문서 샘플 카테고리",
"en": "API Doc Sample Category"
},
"description": null,
"localized_name": "API 문서 샘플 카테고리",
"parent_id": null,
"path": "1",
"depth": 0,
"sort_order": 0,
"is_active": true,
"slug": "apidoc-sample-category",
"url": "apidoc-sample-category",
"icon": "folder",
"meta_title": null,
"meta_description": null,
"created_at": "2026-07-08 01:44:49",
"updated_at": "2026-07-08 01:44:49",
"images": [],
"products_count": 0,
"children_count": 0,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -251,8 +312,8 @@ Content-Type: application/json
| temp_key | body | string | 아니오 | max 64 | 사전 업로드한 임시 이미지를 이 카테고리에 연결하기 위한 FileUploader temp_key |
| collection | body | string | 아니오 | max 100 | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) |
| alt_text | body | array | 아니오 | — | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) |
| alt_text.ko | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| alt_text.en | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| alt_text.ko | body | string | 아니오 | max 255 | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) — `ko` 로케일 값 |
| alt_text.en | body | string | 아니오 | max 255 | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) — `en` 로케일 값 |
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category-image.filter_upload_validation_rules`).
@@ -295,19 +356,56 @@ Content-Disposition: form-data; name="alt_text.en"
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: FileUploader 컴포넌트가 `response.data?.data` 형식을 기대하므로 컨트롤러가 `data` 안에 한 번 더 `data` 객체를 감싸 반환합니다. 아래는 `data.data` 객체의 필드입니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 업로드된 카테고리 이미지의 기본 키 |
| hash | string | `9f2c1b8e4a...` | 이미지 고유 해시 (다운로드 URL 식별자) |
| original_filename | string | `banner.png` | 업로드 당시의 원본 파일명 |
| mime_type | string | `image/png` | 파일 MIME 타입 |
| size | integer | `102400` | 파일 크기 (바이트) |
| size_formatted | string | `100 KB` | 사람이 읽기 쉬운 형식으로 변환한 파일 크기 |
| download_url | string | `/api/modules/sirsoft-ecommerce/admin/categories/images/download/9f2c1b8e4a...` | 이미지 다운로드 URL |
| order | integer | `1` | 이미지 표시 순서 (`sort_order`, 미설정 시 1) |
| is_image | boolean | `true` | MIME 타입이 `image/` 로 시작하는지 여부 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "sirsoft-ecommerce::messages.category_images.uploaded",
"data": {
"data": {
"id": 1,
"hash": "9f2c1b8e4a7d5c0361b2e8f4a9d7c015",
"original_filename": "banner.png",
"mime_type": "image/png",
"size": 102400,
"size_formatted": "100 KB",
"download_url": "/api/modules/sirsoft-ecommerce/admin/categories/images/download/9f2c1b8e4a7d5c0361b2e8f4a9d7c015",
"order": 1,
"is_image": true
}
}
}
```
> `message` 는 `sirsoft-ecommerce::messages.category_images.uploaded` 키를 사용하는데, 현재 모듈 `lang/ko/messages.php` 에 `category_images` 섹션이 정의되어 있지 않아 번역되지 않은 키 문자열이 그대로 반환됩니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 파일 저장 중 예외 발생 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `file` 은 필수이며 `jpeg,png,jpg,gif,svg,webp` 이미지만 허용, 최대 10MB |
<!-- @generated:end -->
@@ -346,19 +444,31 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만)._
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "sirsoft-ecommerce::messages.category_images.reordered"
}
```
> `message` 는 `sirsoft-ecommerce::messages.category_images.reordered` 키를 사용하는데, 현재 모듈 `lang/ko/messages.php` 에 `category_images` 섹션이 정의되어 있지 않아 번역되지 않은 키 문자열이 그대로 반환됩니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 순서 갱신 처리 중 예외 발생 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `order` 는 1건 이상 필수이며 각 항목에 정수 `id` 와 0 이상의 정수 `order` 가 필요 |
<!-- @generated:end -->
@@ -388,19 +498,31 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "sirsoft-ecommerce::messages.category_images.deleted"
}
```
> `message` 는 `sirsoft-ecommerce::messages.category_images.deleted` 키를 사용하는데, 현재 모듈 `lang/ko/messages.php` 에 `category_images` 섹션이 정의되어 있지 않아 번역되지 않은 키 문자열이 그대로 반환됩니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 이미지 삭제 처리 중 예외 발생 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | path 의 이미지 `id` 에 해당하는 카테고리 이미지가 없는 경우 (`messages.category_images.not_found`) |
<!-- @generated:end -->
@@ -443,19 +565,31 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만)._
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "sirsoft-ecommerce::messages.categories.order_updated"
}
```
> `message` 는 `sirsoft-ecommerce::messages.categories.order_updated` 키를 사용하는데, 현재 모듈 `lang/ko/messages.php` 의 `categories` 섹션에 `order_updated` 키가 정의되어 있지 않아 번역되지 않은 키 문자열이 그대로 반환됩니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 순서 갱신 트랜잭션 중 예외 발생 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `parent_menus` / `child_menus` 중 최소 하나가 필요하며, 각 항목의 `id` 는 실제 존재하는 카테고리여야 하고 `order` 는 0 이상의 정수 |
<!-- @generated:end -->
@@ -588,8 +722,8 @@ HTTP/1.1 200
| temp_key | body | string | 아니오 | max 64 | 사전 업로드한 임시 이미지를 이 카테고리에 연결하기 위한 FileUploader temp_key |
| collection | body | string | 아니오 | max 100 | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) |
| alt_text | body | array | 아니오 | — | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) |
| alt_text.ko | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| alt_text.en | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| alt_text.ko | body | string | 아니오 | max 255 | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) — `ko` 로케일 값 |
| alt_text.en | body | string | 아니오 | max 255 | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) — `en` 로케일 값 |
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.category-image.filter_upload_validation_rules`).
@@ -632,20 +766,57 @@ Content-Disposition: form-data; name="alt_text.en"
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: FileUploader 컴포넌트가 `response.data?.data` 형식을 기대하므로 컨트롤러가 `data` 안에 한 번 더 `data` 객체를 감싸 반환합니다. 아래는 `data.data` 객체의 필드입니다 (임시 업로드 엔드포인트와 동일 형식, 다만 이미지가 path 의 `categoryId` 에 즉시 귀속됩니다)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 업로드된 카테고리 이미지의 기본 키 |
| hash | string | `9f2c1b8e4a...` | 이미지 고유 해시 (다운로드 URL 식별자) |
| original_filename | string | `banner.png` | 업로드 당시의 원본 파일명 |
| mime_type | string | `image/png` | 파일 MIME 타입 |
| size | integer | `102400` | 파일 크기 (바이트) |
| size_formatted | string | `100 KB` | 사람이 읽기 쉬운 형식으로 변환한 파일 크기 |
| download_url | string | `/api/modules/sirsoft-ecommerce/admin/categories/images/download/9f2c1b8e4a...` | 이미지 다운로드 URL |
| order | integer | `1` | 이미지 표시 순서 (`sort_order`, 미설정 시 1) |
| is_image | boolean | `true` | MIME 타입이 `image/` 로 시작하는지 여부 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "sirsoft-ecommerce::messages.category_images.uploaded",
"data": {
"data": {
"id": 2,
"hash": "1c47e9a5b60d38f27ea4c9d015b7e8f3",
"original_filename": "banner.png",
"mime_type": "image/png",
"size": 102400,
"size_formatted": "100 KB",
"download_url": "/api/modules/sirsoft-ecommerce/admin/categories/images/download/1c47e9a5b60d38f27ea4c9d015b7e8f3",
"order": 1,
"is_image": true
}
}
}
```
> `message` 는 `sirsoft-ecommerce::messages.category_images.uploaded` 키를 사용하는데, 현재 모듈 `lang/ko/messages.php` 에 `category_images` 섹션이 정의되어 있지 않아 번역되지 않은 키 문자열이 그대로 반환됩니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 파일 저장 중 예외 발생 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `file` 은 필수이며 `jpeg,png,jpg,gif,svg,webp` 이미지만 허용, 최대 10MB |
<!-- @generated:end -->
@@ -743,16 +914,78 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`CategoryResource`). 수정 직후 `fresh(['images'])` 로 재조회하므로 `images` 관계만 포함되고 `parent`/`children` 키는 나타나지 않습니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| name | object | `{"ko":"의류","en":"Clothing"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
| description | object\|null | `null` | 설명 (다국어 필드는 로케일별 값 객체, 미설정 시 null) |
| localized_name | string | `의류` | `name` 의 현재 로케일 해석 값 |
| parent_id | integer\|null | `null` | 상위 카테고리 ID (변경 시 depth/path 재계산) |
| path | string | `1` | 조상부터 자기 자신까지의 ID를 `/`로 이은 materialized path |
| depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위) |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| is_active | boolean | `true` | 활성 여부 |
| slug | string | `clothing` | URL 친화 식별자 (slug) |
| url | string | `clothing` | SortableMenuItem 표시용 URL (slug 값을 그대로 사용) |
| icon | string | `folder` | SortableMenuList 표시용 아이콘 (리소스에서 고정값 `folder`) |
| meta_title | string\|null | `null` | SEO 메타 제목 (미설정 시 null) |
| meta_description | string\|null | `null` | SEO 메타 설명 (미설정 시 null) |
| created_at | string | `2026-07-08 01:44:49` | 생성 일시 (`Y-m-d H:i:s`) |
| updated_at | string | `2026-07-08 06:00:17` | 최종 수정 일시 (`Y-m-d H:i:s`) |
| images | array | `[]` | 카테고리 이미지 배열 (각 항목: id/hash/original_filename/mime_type/size/size_formatted/download_url/order/is_image/alt_text) |
| products_count | integer | `0` | 연결된 상품 개수 (집계) |
| children_count | integer | `0` | 하위 카테고리 개수 (집계) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "카테고리가 수정되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "API 문서 샘플 카테고리",
"en": "API Doc Sample Category"
},
"description": null,
"localized_name": "API 문서 샘플 카테고리",
"parent_id": null,
"path": "1",
"depth": 0,
"sort_order": 0,
"is_active": true,
"slug": "apidoc-sample-category",
"url": "apidoc-sample-category",
"icon": "folder",
"meta_title": null,
"meta_description": null,
"created_at": "2026-07-08 01:44:49",
"updated_at": "2026-07-08 06:00:17",
"images": [],
"products_count": 0,
"children_count": 0,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 대상 카테고리가 없거나 수정 트랜잭션 중 예외 발생 (컨트롤러가 예외를 잡아 `exceptions.operation_failed` 로 응답) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
@@ -951,7 +1184,9 @@ HTTP/1.1 200
**에러 응답**
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 500 | Internal Server Error | 카테고리 트리 조회 중 예외가 발생한 경우 (컨트롤러가 예외를 잡아 `messages.categories.fetch_failed` 로 응답) |
<!-- @generated:end -->
@@ -39,17 +39,41 @@ Accept: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 JSON 봉투(`data`)를 반환하지 않습니다. 성공 시 이미지 원본 바이너리를 `StreamedResponse` 로 스트리밍합니다 (`CategoryImageService::download()` → `StorageInterface::response()`)._
| 응답 요소 | 값 | 용도/설명 |
| --- | --- | --- |
| 본문 | 이미지 원본 바이너리 | `category_images.path` 에 저장된 파일을 스토리지에서 스트리밍 |
| `Content-Type` 헤더 | `image/jpeg` (레코드의 `mime_type` 값) | 업로드 시 저장된 MIME 타입을 그대로 사용 |
| `Cache-Control` 헤더 | `public, max-age=31536000` | 브라우저/CDN 1년 캐싱 |
| `Content-Disposition` 헤더 | 원본 파일명(`original_filename`) 포함 | 다운로드 시 표시될 파일명 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200 OK
Content-Type: image/jpeg
Cache-Control: public, max-age=31536000
Content-Disposition: inline; filename="category-main.jpg"
<이미지 바이너리 데이터>
```
실패 시에는 JSON 봉투로 응답합니다.
```json
{
"success": false,
"message": "카테고리 이미지를 찾을 수 없습니다."
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 400 | Bad Request | 스트리밍 처리 중 예외 발생 (`exceptions.operation_failed` → "작업 처리 중 오류가 발생했습니다.") |
| 404 | Not Found | `hash` 에 해당하는 카테고리 이미지 레코드가 없거나, 레코드는 있으나 스토리지에 실제 파일이 없는 경우 (`CategoryImageService::download()` 가 `null` 반환) |
<!-- @generated:end -->
@@ -38,7 +38,7 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만) — `CheckoutController@destroy` 는 `ResponseHelper::moduleSuccess('sirsoft-ecommerce', 'messages.checkout.deleted')` 를 데이터 인수 없이 호출합니다._
**응답 예시**
@@ -51,14 +51,17 @@ HTTP/1.1 200
```json
{
"success": true,
"message": "Checkout has been deleted.",
"message": "주문서가 삭제되었습니다.",
"data": null
}
```
**에러 응답**
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | 삭제할 임시 주문이 없음 (`TempOrderService::deleteTempOrder()` 가 false 반환 — 이미 만료·미존재). 메시지: `임시 주문 정보를 찾을 수 없습니다.` |
| 500 | Internal Server Error | 삭제 처리 중 예외 발생. 메시지: `주문서 삭제에 실패했습니다.` |
<!-- @generated:end -->
@@ -92,17 +95,256 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`CheckoutDataService::buildResponseData()` 가 구성)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| temp_order_id | integer | `1` | 임시 주문 ID (주문 확정 시 사용) |
| items | array | `[...]` | 주문 상품 목록 (`CheckoutItemResource` — 아래 "items[] 항목" 표) |
| calculation | object | `{...}` | 주문 계산 결과 (`OrderCalculationResult::toArray()` — 아래 "calculation" 표) |
| promotions | object | `{"item_coupons":{},"order_coupon_issue_id":null,"shipping_coupon_issue_id":null}` | 현재 임시 주문에 적용된 쿠폰 선택 상태 |
| use_points | integer | `0` | 현재 사용 설정된 마일리지 포인트 |
| shipping_address | object\|null | `{"country_code":"KR","zipcode":"06234","region":null,"city":null,"address":null}` | 배송비 계산에 사용된 배송 주소 (미전달 시 `null`) |
| expires_at | string | `2026-07-14T12:34:56+09:00` | 임시 주문 만료 시각 (ISO 8601) |
| available_coupons | array | `[]` | 주문금액/배송비 대상 사용 가능 쿠폰 목록 (비회원은 빈 배열) |
| mileage | object\|null | `{...}` | 마일리지 잔액 정보 (비회원은 `null` — 아래 "mileage" 표) |
| validation_errors | array | `[]` | 조건 위반으로 자동 제외된 쿠폰의 사유 목록 (min_amount / per_user_limit / not_combinable 등) |
| has_unshippable_items | boolean | `false` | 선택된 배송국가로 배송 불가한 상품이 1개라도 있으면 `true` (주문하기 차단 플래그) |
| selected_shipping_country | string | `KR` | 현재 선택된 배송 국가 코드 |
| free_shipping | object | `{...}` | 무료배송 기준액 정보 (아래 "free_shipping" 표) |
| unavailable_items | array | `[]` | 구매 불가 상품 목록 (존재할 때만 포함) |
| has_stock_issue | boolean | `false` | 구매 불가 사유에 재고 부족이 포함되면 `true` (`unavailable_items` 존재 시에만 포함) |
| has_status_issue | boolean | `false` | 구매 불가 사유에 판매 상태 문제가 포함되면 `true` (`unavailable_items` 존재 시에만 포함) |
`items[]` 항목 (`CheckoutItemResource::toArray()`):
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer\|null | `1` | 장바구니 아이템 ID (바로구매는 `null`) |
| product_id | integer | `1` | 상품 ID |
| product_option_id | integer | `1` | 상품 옵션 ID |
| quantity | integer | `1` | 주문 수량 |
| additional_options | array | `[]` | 선택한 추가 옵션 (`group_name` / `name` / `price_adjustment`) |
| additional_options_total | integer | `0` | 추가 옵션 단가 합계 |
| product | object\|null | `{...}` | 상품 정보 (이름·썸네일 등) |
| product_option | object\|null | `{...}` | 옵션 정보 (옵션명·판매가 등) |
| is_shippable_to_selected_country | boolean | `true` | 선택된 배송국가로 이 상품이 배송 가능한지 |
| subtotal | integer | `10000` | 표시 소계 = (옵션 판매가 + 추가옵션 단가 합계) × 수량 |
| subtotal_formatted | string | `10,000원` | 소계 통화 포맷 |
| multi_currency_subtotal | object | `{"KRW":{...}}` | 소계 다통화 환산 |
| unit_price | integer | `10000` | 계산 기준 단가 |
| unit_price_formatted | string | `10,000원` | 단가 통화 포맷 |
| product_coupon_discount_amount | integer | `0` | 상품 쿠폰 할인액 |
| product_coupon_discount_formatted | string | `0원` | 상품 쿠폰 할인액 포맷 |
| code_discount_amount | integer | `0` | 할인코드 할인액 |
| code_discount_formatted | string | `0원` | 할인코드 할인액 포맷 |
| order_coupon_discount_share | integer | `0` | 주문 쿠폰 할인의 이 라인 분담액 |
| order_coupon_discount_share_formatted | string | `0원` | 분담액 포맷 |
| total_discount | integer | `0` | 이 라인의 총 할인액 |
| total_discount_formatted | string | `0원` | 총 할인액 포맷 |
| points_used_share | integer | `0` | 사용 마일리지의 이 라인 분담액 |
| points_used_share_formatted | string | `0원` | 분담액 포맷 |
| points_earning | integer | `0` | 이 라인의 적립 예정 마일리지 |
| final_amount | integer | `10000` | 이 라인의 최종 금액 |
| final_amount_formatted | string | `10,000원` | 최종 금액 포맷 |
| multi_currency_unit_price | object | `{"KRW":{"amount":10000,"formatted":"10,000원"}}` | 단가 다통화 환산 |
| multi_currency_product_coupon_discount | object | `{"KRW":{"amount":0,"formatted":"0원"}}` | 상품 쿠폰 할인 다통화 환산 |
| multi_currency_total_discount | object | `{"KRW":{"amount":0,"formatted":"0원"}}` | 총 할인 다통화 환산 |
| multi_currency_final_amount | object | `{"KRW":{"amount":10000,"formatted":"10,000원"}}` | 최종 금액 다통화 환산 |
| available_coupons | array | `[]` | 이 상품에 적용 가능한 상품 쿠폰 목록 |
| disabled_coupon_ids | array | `[]` | 다른 상품 라인에서 이미 소진되어(per_user_limit) 이 라인에서는 선택 불가한 쿠폰 발급 ID |
`calculation` (`OrderCalculationResult::toArray()`):
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| items | array | `[...]` | 라인별 계산 결과 |
| summary | object | `{...}` | 합계 정보 (`Summary::toArray()` — 아래 표) |
| promotions | object | `{...}` | 실제 적용된 프로모션 요약 (상품/주문 프로모션 분리 + `coupon_issue_ids` / `item_coupons` / `discount_code` 평탄 키) |
| validation_errors | array | `[]` | 쿠폰 검증 오류 목록 |
| metadata | object | `{}` | 플러그인 확장용 메타데이터 (비어 있으면 생략) |
`calculation.summary` (`Summary::toArray()`):
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| subtotal | integer | `10000` | 상품 금액 소계 (할인 전) |
| subtotal_formatted | string | `10,000원` | 소계 통화 포맷 |
| product_coupon_discount | integer | `0` | 상품/카테고리 쿠폰 할인 합계 |
| product_coupon_discount_formatted | string | `0원` | 상품 쿠폰 할인 포맷 |
| code_discount | integer | `0` | 할인코드 할인 합계 |
| code_discount_formatted | string | `0원` | 할인코드 할인 포맷 |
| order_coupon_discount | integer | `0` | 주문 쿠폰 할인 합계 |
| order_coupon_discount_formatted | string | `0원` | 주문 쿠폰 할인 포맷 |
| total_coupon_discount | integer | `0` | 상품 + 주문 쿠폰 할인 합계 |
| total_coupon_discount_formatted | string | `0원` | 쿠폰 할인 합계 포맷 |
| total_discount | integer | `0` | 총 할인금액 |
| discount_formatted | string | `0원` | 총 할인금액 포맷 |
| base_shipping_total | integer | `3000` | 기본 배송비 합계 |
| extra_shipping_total | integer | `0` | 추가 배송비(도서산간) 합계 |
| total_shipping | integer | `3000` | 총 배송비 (기본 + 추가) |
| total_shipping_fee | integer | `3000` | 총 배송비 (별칭 키) |
| shipping_fee_formatted | string | `3,000원` | 총 배송비 포맷 |
| shipping_discount | integer | `0` | 배송비 할인 |
| shipping_discount_formatted | string | `0원` | 배송비 할인 포맷 |
| taxable_amount | integer | `10000` | 과세 금액 합계 |
| tax_free_amount | integer | `0` | 면세 금액 합계 |
| points_earning | integer | `0` | 적립 예정 마일리지 합계 |
| total_mileage | integer | `0` | 적립 예정 마일리지 (별칭 키) |
| mileage_formatted | string | `0P` | 적립 예정 마일리지 포맷 |
| points_used | integer | `0` | 사용 마일리지 합계 |
| points_used_formatted | string | `0원` | 사용 마일리지 차감액 통화 포맷 |
| payment_amount | integer | `13000` | 결제금액 (마일리지 사용 전) |
| payment_amount_formatted | string | `13,000원` | 결제금액 포맷 |
| final_amount | integer | `13000` | 최종 지불금액 |
| final_amount_formatted | string | `13,000원` | 최종 지불금액 포맷 |
| selected_payment_currency | string | `KRW` | 선택된 결제 통화 (설정된 경우에만 포함) |
| multi_currency | object | `{"KRW":{...}}` | 합계 금액의 다통화 환산 (설정된 경우에만 포함) |
| coupon_discount | integer | `0` | 상품 쿠폰 할인 (deprecated — 하위 호환) |
| coupon_discount_formatted | string | `0원` | 상품 쿠폰 할인 포맷 (deprecated) |
| order_discount | integer | `0` | 주문 쿠폰 할인 (deprecated — 하위 호환) |
| order_discount_formatted | string | `0원` | 주문 쿠폰 할인 포맷 (deprecated) |
`mileage` (`UserMileageService::getBalance()` + 체크아웃 추가 키, 비회원은 `null`):
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| enabled | boolean | `true` | 마일리지 적립 기능 활성화 여부 |
| available | integer | `0` | 사용 가능 마일리지 잔액 |
| pending | integer | `0` | 적립 대기 중인 마일리지 |
| expiring_soon | integer | `0` | 곧 소멸 예정 마일리지 |
| expiring_date | string\|null | `2026-08-31` | 소멸 예정일 (없으면 `null`) |
| total_earned | integer | `0` | 누적 적립 마일리지 |
| total_used | integer | `0` | 누적 사용 마일리지 |
| by_currency | object | `{}` | 통화별 마일리지 내역 |
| max_usable | integer | `0` | 이번 주문에서 사용 가능한 최대 마일리지 |
| usable | boolean | `true` | 마일리지 "사용" 가능 여부 (기본 통화 사용 규칙 미설정 시 `false`) |
`free_shipping` (`CheckoutDataService::buildFreeShippingInfo()`):
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| enabled | boolean | `false` | 무료배송 기준액 기능 활성화 여부 |
| threshold_base | integer | `50000` | 무료배송 기준액 (기본 통화 정수) |
| threshold_multi_currency | object | `{"KRW":{...}}` | 기준액의 결제통화 환산 (기준액 0 이면 `[]`) |
| remaining_base | integer | `40000` | 무료배송까지 남은 금액 (기본 통화) |
| remaining_multi_currency | object | `{"KRW":{...}}` | 남은 금액의 결제통화 환산 (남은 금액 0 이면 `[]`) |
**응답 예시**
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "주문서 정보를 조회했습니다.",
"data": {
"temp_order_id": 1,
"items": [
{
"id": 1,
"product_id": 1,
"product_option_id": 1,
"quantity": 1,
"additional_options": [],
"additional_options_total": 0,
"product": { "id": 1, "name": "샘플 상품" },
"product_option": { "id": 1, "name": "기본 옵션" },
"is_shippable_to_selected_country": true,
"subtotal": 10000,
"subtotal_formatted": "10,000원",
"multi_currency_subtotal": {},
"unit_price": 10000,
"unit_price_formatted": "10,000원",
"product_coupon_discount_amount": 0,
"product_coupon_discount_formatted": "0원",
"code_discount_amount": 0,
"code_discount_formatted": "0원",
"order_coupon_discount_share": 0,
"order_coupon_discount_share_formatted": "0원",
"total_discount": 0,
"total_discount_formatted": "0원",
"points_used_share": 0,
"points_used_share_formatted": "0원",
"points_earning": 0,
"final_amount": 10000,
"final_amount_formatted": "10,000원",
"available_coupons": [],
"disabled_coupon_ids": []
}
],
"calculation": {
"items": [],
"summary": {
"subtotal": 10000,
"subtotal_formatted": "10,000원",
"total_discount": 0,
"discount_formatted": "0원",
"base_shipping_total": 3000,
"extra_shipping_total": 0,
"total_shipping": 3000,
"shipping_fee_formatted": "3,000원",
"points_earning": 0,
"points_used": 0,
"points_used_formatted": "0원",
"payment_amount": 13000,
"payment_amount_formatted": "13,000원",
"final_amount": 13000,
"final_amount_formatted": "13,000원"
},
"promotions": {
"coupon_issue_ids": [],
"item_coupons": {},
"discount_code": null
},
"validation_errors": []
},
"promotions": {
"item_coupons": {},
"order_coupon_issue_id": null,
"shipping_coupon_issue_id": null
},
"use_points": 0,
"shipping_address": {
"country_code": "KR",
"zipcode": "06234",
"region": null,
"city": null,
"address": null
},
"expires_at": "2026-07-14T12:34:56+09:00",
"available_coupons": [],
"mileage": {
"enabled": true,
"available": 0,
"pending": 0,
"expiring_soon": 0,
"expiring_date": null,
"total_earned": 0,
"total_used": 0,
"by_currency": {},
"max_usable": 0,
"usable": true
},
"validation_errors": [],
"has_unshippable_items": false,
"selected_shipping_country": "KR",
"free_shipping": {
"enabled": false,
"threshold_base": 0,
"threshold_multi_currency": [],
"remaining_base": 0,
"remaining_multi_currency": []
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | 임시 주문이 만료되었거나 존재하지 않음. 메시지: `임시 주문 정보를 찾을 수 없습니다.` |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 조회/재계산 중 예외 발생 |
<!-- @generated:end -->
@@ -153,17 +395,52 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`CheckoutController@store` 가 직접 조립 — 성공 시 HTTP 201)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| temp_order_id | integer | `1` | 생성된 임시 주문 ID (이후 조회/수정/주문 확정에 사용) |
| calculation | object | `{...}` | 주문 계산 결과 (`OrderCalculationResult::toArray()` — `items` / `summary` / `promotions` / `validation_errors`. 상세는 GET /checkout 의 calculation 표 참조) |
| expires_at | string | `2026-07-14T12:34:56+09:00` | 임시 주문 만료 시각 (ISO 8601) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "주문서가 생성되었습니다.",
"data": {
"temp_order_id": 1,
"calculation": {
"items": [],
"summary": {
"subtotal": 10000,
"subtotal_formatted": "10,000원",
"total_discount": 0,
"total_shipping": 3000,
"payment_amount": 13000,
"final_amount": 13000,
"final_amount_formatted": "13,000원"
},
"promotions": {
"coupon_issue_ids": [],
"item_coupons": {},
"discount_code": null
},
"validation_errors": []
},
"expires_at": "2026-07-14T12:34:56+09:00"
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 400 | Bad Request | 장바구니가 비어 있음. 메시지: `장바구니가 비어있습니다.` |
| 422 | Unprocessable Entity | 보유 잔액을 초과하는 마일리지 사용 요청 (`error.code: mileage_exceeds_balance`). 메시지: `주문서 생성에 실패했습니다.` |
| 500 | Internal Server Error | 임시 주문 생성 중 예외 발생. 메시지: `주문서 생성에 실패했습니다.` |
<!-- @generated:end -->
@@ -216,17 +493,93 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드. GET `/api/modules/sirsoft-ecommerce/checkout` 과 동일한 구조입니다 (`CheckoutDataService::buildResponseData()` 를 그대로 사용)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| temp_order_id | integer | `1` | 임시 주문 ID |
| items | array | `[...]` | 재계산된 주문 상품 목록 (`CheckoutItemResource` — GET 문서의 `items[]` 표와 동일) |
| calculation | object | `{...}` | 재계산된 주문 계산 결과 (`items` / `summary` / `promotions` / `validation_errors`) |
| promotions | object | `{"item_coupons":{},"order_coupon_issue_id":null,"shipping_coupon_issue_id":null}` | 반영된 쿠폰 선택 상태 (미전송 필드는 기존 값 유지) |
| use_points | integer | `0` | 반영된 마일리지 사용 포인트 |
| shipping_address | object\|null | `{"country_code":"KR","zipcode":"06234"}` | 배송비 계산에 사용된 배송 주소 |
| expires_at | string | `2026-07-14T12:34:56+09:00` | 임시 주문 만료 시각 (ISO 8601) |
| available_coupons | array | `[]` | 주문금액/배송비 대상 사용 가능 쿠폰 (비회원은 빈 배열) |
| mileage | object\|null | `{...}` | 마일리지 잔액 정보 (비회원은 `null`) |
| validation_errors | array | `[]` | 조건 위반으로 자동 제외된 쿠폰의 사유 목록 |
| has_unshippable_items | boolean | `false` | 선택된 배송국가로 배송 불가한 상품 존재 여부 |
| selected_shipping_country | string | `KR` | 현재 선택된 배송 국가 코드 |
| free_shipping | object | `{...}` | 무료배송 기준액 정보 (`enabled` / `threshold_base` / `threshold_multi_currency` / `remaining_base` / `remaining_multi_currency`) |
**응답 예시**
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "주문서가 수정되었습니다.",
"data": {
"temp_order_id": 1,
"items": [],
"calculation": {
"items": [],
"summary": {
"subtotal": 10000,
"subtotal_formatted": "10,000원",
"total_discount": 1000,
"discount_formatted": "1,000원",
"total_shipping": 3000,
"shipping_fee_formatted": "3,000원",
"points_used": 0,
"payment_amount": 12000,
"final_amount": 12000,
"final_amount_formatted": "12,000원"
},
"promotions": {
"coupon_issue_ids": [10],
"item_coupons": {},
"discount_code": null
},
"validation_errors": []
},
"promotions": {
"item_coupons": {},
"order_coupon_issue_id": 10,
"shipping_coupon_issue_id": null
},
"use_points": 0,
"shipping_address": {
"country_code": "KR",
"zipcode": "06234"
},
"expires_at": "2026-07-14T12:34:56+09:00",
"available_coupons": [],
"mileage": {
"enabled": true,
"available": 0,
"max_usable": 0,
"usable": true
},
"validation_errors": [],
"has_unshippable_items": false,
"selected_shipping_country": "KR",
"free_shipping": {
"enabled": false,
"threshold_base": 0,
"threshold_multi_currency": [],
"remaining_base": 0,
"remaining_multi_currency": []
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 404 | Not Found | 임시 주문이 만료되었거나 존재하지 않음 (`TempOrderNotFoundException`). 메시지: `임시 주문 정보를 찾을 수 없습니다.` |
| 422 | Unprocessable Entity | 보유 잔액을 초과하는 마일리지 사용 요청 (`error.code: mileage_exceeds_balance`). 메시지: `주문서 수정에 실패했습니다.` |
| 500 | Internal Server Error | 재계산 중 예외 발생. 메시지: `주문서 수정에 실패했습니다.` |
<!-- @generated:end -->
@@ -254,15 +607,30 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| expires_at | string | `2026-07-14T13:04:56+09:00` | 연장된 임시 주문 만료 시각 (ISO 8601) |
**응답 예시**
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "주문서 유효시간이 연장되었습니다.",
"data": {
"expires_at": "2026-07-14T13:04:56+09:00"
}
}
```
**에러 응답**
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | 연장할 임시 주문이 이미 만료되었거나 존재하지 않음 (`TempOrderService::extendExpiration()` 이 null 반환). 메시지: `임시 주문 정보를 찾을 수 없습니다.` |
| 500 | Internal Server Error | 연장 처리 중 예외 발생. 메시지: `주문서 유효시간 연장에 실패했습니다.` |
<!-- @generated:end -->
@@ -192,11 +192,58 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ClaimReasonResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `8` | 생성된 클래임 사유의 기본 키 (내부 식별자) |
| type | string | `refund` | 클래임 사유 유형 (ClaimReasonTypeEnum — `refund`(환불/취소)) |
| code | string | `apidoc_sample` | 사유 식별 코드 (같은 type 내 고유) |
| name | object | `{"ko":"API 문서 샘플 사유","en":"API Doc Sample Reason"}` | 다국어 사유명 (로케일별 값 객체) |
| localized_name | string | `API 문서 샘플 사유` | `name` 의 현재 로케일 해석 값 |
| fault_type | string | `customer` | 귀책 구분 (ClaimReasonFaultTypeEnum — `customer`(고객)/`seller`(판매자)/`carrier`(배송사)) |
| fault_type_label | string | `고객 귀책` | `fault_type` 값의 사람이 읽는 라벨 (Enum label 파생) |
| is_user_selectable | boolean | `true` | 회원 취소/반품 신청 화면에서 선택지로 노출할지 여부 |
| is_active | boolean | `true` | 활성 여부 (false 면 선택지에서 제외) |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (`sirsoft-ecommerce.settings.update` 권한 기반) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "클래임 사유가 등록되었습니다.",
"data": {
"id": 8,
"type": "refund",
"code": "apidoc_sample",
"name": {
"ko": "API 문서 샘플 사유",
"en": "API Doc Sample Reason"
},
"localized_name": "API 문서 샘플 사유",
"fault_type": "customer",
"fault_type_label": "고객 귀책",
"is_user_selectable": true,
"is_active": true,
"sort_order": 0,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 10:44:49",
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -204,7 +251,7 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 같은 `type` 내에 동일한 `code` 가 이미 존재하면 `code` 필드 검증 실패 |
<!-- @generated:end -->
@@ -401,20 +448,68 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ClaimReasonResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `8` | 수정된 클래임 사유의 기본 키 (내부 식별자) |
| type | string | `refund` | 클래임 사유 유형 (ClaimReasonTypeEnum — `refund`(환불/취소)) |
| code | string | `apidoc_sample` | 사유 식별 코드 (같은 type 내 고유) |
| name | object | `{"ko":"API 문서 샘플 사유(수정)","en":"API Doc Sample Reason (Updated)"}` | 다국어 사유명 (로케일별 값 객체) |
| localized_name | string | `API 문서 샘플 사유(수정)` | `name` 의 현재 로케일 해석 값 |
| fault_type | string | `seller` | 귀책 구분 (ClaimReasonFaultTypeEnum — `customer`(고객)/`seller`(판매자)/`carrier`(배송사)) |
| fault_type_label | string | `판매자 귀책` | `fault_type` 값의 사람이 읽는 라벨 (Enum label 파생) |
| is_user_selectable | boolean | `true` | 회원 취소/반품 신청 화면에서 선택지로 노출할지 여부 |
| is_active | boolean | `true` | 활성 여부 (false 면 선택지에서 제외) |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 15:00:18` | 최종 수정 일시 |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (`sirsoft-ecommerce.settings.update` 권한 기반) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "클래임 사유가 수정되었습니다.",
"data": {
"id": 8,
"type": "refund",
"code": "apidoc_sample",
"name": {
"ko": "API 문서 샘플 사유(수정)",
"en": "API Doc Sample Reason (Updated)"
},
"localized_name": "API 문서 샘플 사유(수정)",
"fault_type": "seller",
"fault_type_label": "판매자 귀책",
"is_user_selectable": true,
"is_active": true,
"sort_order": 0,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 15:00:18",
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 대상 사유가 없거나 갱신에 실패한 경우 (서비스 예외 → `exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 같은 `type` 내 다른 사유가 동일한 `code` 를 이미 사용 중이면 `code` 필드 검증 실패 |
<!-- @generated:end -->
@@ -534,19 +534,55 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (생성된 쿠폰 발급 내역 `CouponIssue` 모델 직렬화 — 전용 Resource 를 거치지 않고 모델의 컬럼/캐스트가 그대로 노출됩니다)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `8611` | 발급 내역 ID (기본 키) |
| coupon_id | integer | `157` | 발급된 쿠폰 정의(마스터)의 식별자 |
| user_id | integer | `1` | 발급받은 회원 식별자 |
| coupon_code | string | `DL-8QK2ZX4M` | 발급 시 생성된 쿠폰 코드 (다운로드 발급은 `DL-` + 8자리 랜덤 대문자) |
| status | string | `available` | 발급 건 상태 (available=사용가능, used=사용완료, expired=만료, cancelled=취소) |
| issued_at | string | `2026-07-14T02:24:18.000000Z` | 발급일시 (발급 처리 시각) |
| expired_at | string \| null | `2026-08-07T02:24:18.000000Z` | 만료일시 (valid_type=period 이면 쿠폰의 `valid_to`, days_from_issue 이면 발급일 + `valid_days`, 둘 다 아니면 null) |
| used_at | null | `null` | 사용일시 (발급 직후에는 항상 null) |
| order_id | null | `null` | 사용된 주문 식별자 (발급 직후에는 항상 null) |
| discount_amount | string \| null | `null` | 실제 적용된 할인 금액 (사용 전에는 null, decimal(15,2)) |
| created_at | string | `2026-07-14T02:24:18.000000Z` | 생성 일시 |
| updated_at | string | `2026-07-14T02:24:18.000000Z` | 최종 수정 일시 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "쿠폰이 다운로드되었습니다.",
"data": {
"coupon_id": 157,
"user_id": 1,
"coupon_code": "DL-8QK2ZX4M",
"status": "available",
"issued_at": "2026-07-14T02:24:18.000000Z",
"expired_at": "2026-08-07T02:24:18.000000Z",
"updated_at": "2026-07-14T02:24:18.000000Z",
"created_at": "2026-07-14T02:24:18.000000Z",
"id": 8611
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | 쿠폰 다운로드 제한 횟수를 초과했습니다. | 회원 1인당 발급 제한(`per_user_limit`) 을 이미 채운 경우 |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | `coupon_id` 가 정수가 아니거나 존재하지 않는 쿠폰인 경우 (`error.errors` 에 필드별 메시지) |
<!-- @generated:end -->
@@ -99,18 +99,35 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| preferred_currency | string | `USD` | 저장된 선호 결제 통화 코드 (요청의 `currency` 값이 그대로 반환됨) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "결제 통화가 변경되었습니다.",
"data": {
"preferred_currency": "USD"
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | `currency` 누락("결제 통화를 선택해 주세요.") 또는 등록되지 않은/환율 미설정 통화 지정("등록된 통화만 선택할 수 있습니다.") |
| 500 | Internal Server Error | 통화 저장 중 예외 발생 시 ("결제 통화 변경에 실패했습니다.") |
<!-- @generated:end -->
@@ -203,19 +203,62 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ExtraFeeTemplateResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| zipcode | string | `06234` | 추가배송비를 적용할 우편번호 (단일 또는 `-` 로 이은 범위) |
| fee | integer | `3000` | 해당 우편번호에 부과할 추가 배송비 (상점 기본 통화 기준 반올림) |
| fee_formatted | string | `3,000원` | `fee` 값의 표시용 포맷 문자열 (기본 통화 포맷) |
| region | string \| null | `경기 안산 풍도동` | 지역명 (도서산간 등 관리자 참고용 표시 라벨, 미입력 시 null) |
| description | string \| null | `도서산간 지역` | 설명 (미입력 시 null) |
| is_active | boolean | `true` | 활성 여부 (true 이면 배송비 계산에 할증 반영) |
| created_by | string \| null | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 등록자 UUID (creator 관계 파생, 없으면 null) |
| updated_by | string \| null | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 최종 수정자 UUID (updater 관계 파생, 없으면 null) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 (사용자 타임존 기준 포맷) |
| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 (사용자 타임존 기준 포맷) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (모두 `sirsoft-ecommerce.settings.update` 권한 기반) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "추가배송비 템플릿이 등록되었습니다.",
"data": {
"id": 1,
"zipcode": "06234",
"fee": 3000,
"fee_formatted": "3,000원",
"region": "경기 안산 풍도동",
"description": "도서산간 지역",
"is_active": true,
"created_by": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"updated_by": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 10:44:49",
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 저장 중 예외 발생 시 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.create`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `zipcode` 는 숫자 또는 `숫자-숫자` 범위 형식이어야 하며 이미 등록된 우편번호면 중복 오류 |
<!-- @generated:end -->
@@ -479,19 +522,38 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (일괄 삭제 결과 요약)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted_count | integer | `3` | 실제로 삭제된 템플릿 건수 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": ":count개 추가배송비 템플릿이 삭제되었습니다.",
"data": {
"deleted_count": 3
}
}
```
> 컨트롤러가 메시지 치환 파라미터를 전달하지 않으므로 `message` 의 `:count` 자리표시자는 치환되지 않은 채로 내려갑니다. 실제 삭제 건수는 `data.deleted_count` 를 사용하세요.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 삭제 중 예외 발생 시 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `ids` 는 1건 이상이어야 하며 각 값은 존재하는 템플릿 ID 여야 함 |
<!-- @generated:end -->
@@ -528,19 +590,38 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (일괄 등록 결과 요약)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| created_count | integer | `2` | 실제로 등록(또는 갱신)된 템플릿 건수 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": ":count개 추가배송비 템플릿이 등록되었습니다.",
"data": {
"created_count": 2
}
}
```
> 컨트롤러가 메시지 치환 파라미터를 전달하지 않으므로 `message` 의 `:count` 자리표시자는 치환되지 않은 채로 내려갑니다. 실제 등록 건수는 `data.created_count` 를 사용하세요.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 일괄 등록 중 예외 발생 시 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.create`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 항목별로 `items.*.zipcode` (필수, 숫자 또는 `숫자-숫자` 범위, 최대 20자) 와 `items.*.fee` (필수, 0 이상) 가 검증됨 |
<!-- @generated:end -->
@@ -579,19 +660,38 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (일괄 변경 결과 요약)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `3` | 활성 여부가 실제로 변경된 템플릿 건수 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": ":count개 추가배송비 템플릿의 사용여부가 변경되었습니다.",
"data": {
"updated_count": 3
}
}
```
> 컨트롤러가 메시지 치환 파라미터를 전달하지 않으므로 `message` 의 `:count` 자리표시자는 치환되지 않은 채로 내려갑니다. 실제 변경 건수는 `data.updated_count` 를 사용하세요.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 일괄 변경 중 예외 발생 시 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `ids` 는 1건 이상이며 각 값은 존재하는 템플릿 ID, `is_active` 는 필수 boolean |
<!-- @generated:end -->
@@ -621,7 +721,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
@@ -719,20 +819,63 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ExtraFeeTemplateResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| zipcode | string | `06234` | 추가배송비를 적용할 우편번호 (단일 또는 `-` 로 이은 범위) |
| fee | integer | `5000` | 해당 우편번호에 부과할 추가 배송비 (상점 기본 통화 기준 반올림) |
| fee_formatted | string | `5,000원` | `fee` 값의 표시용 포맷 문자열 (기본 통화 포맷) |
| region | string \| null | `경기 안산 풍도동` | 지역명 (도서산간 등 관리자 참고용 표시 라벨, 미입력 시 null) |
| description | string \| null | `도서산간 지역` | 설명 (미입력 시 null) |
| is_active | boolean | `true` | 활성 여부 (true 이면 배송비 계산에 할증 반영) |
| created_by | string \| null | `null` | 등록자 UUID (creator 관계 파생, 없으면 null) |
| updated_by | string \| null | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 최종 수정자 UUID (수정 시 현재 로그인 사용자로 갱신) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 (사용자 타임존 기준 포맷) |
| updated_at | string | `2026-07-08 15:00:20` | 최종 수정 일시 (사용자 타임존 기준 포맷) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (모두 `sirsoft-ecommerce.settings.update` 권한 기반) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "추가배송비 템플릿이 수정되었습니다.",
"data": {
"id": 1,
"zipcode": "06234",
"fee": 5000,
"fee_formatted": "5,000원",
"region": "경기 안산 풍도동",
"description": "도서산간 지역",
"is_active": true,
"created_by": null,
"updated_by": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 15:00:20",
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 수정 중 예외 발생 시 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `zipcode` 는 숫자 또는 `숫자-숫자` 범위 형식이어야 하며 자신을 제외한 다른 템플릿과 중복될 수 없음 |
<!-- @generated:end -->
@@ -48,16 +48,38 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| guest_order_token | string | `eyJpdiI6...` | 비회원 주문 조회 토큰 (30분 유효). 이후 비회원 주문 상세/취소/환불예상 호출 시 `X-Guest-Order-Token` 헤더로 전달 |
| expires_at | string | `2026-07-14T15:02:11+09:00` | 토큰 만료 일시 (ISO 8601) |
| order | object | — | 최소 주문 요약 (상세는 토큰 보호 엔드포인트에서 제공) |
| order.order_number | string | `20260714000012` | 인증된 주문의 주문번호 |
| order.order_status | string | `payment_complete` | 주문 상태 (`pending_order`, `pending_payment`, `payment_complete`, `shipping_hold`, `preparing`, `shipping_ready`, `shipping`, `delivered`, `confirmed`, `cancelled`) |
**응답 예시**
<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "주문 정보를 조회했습니다.",
"data": {
"guest_order_token": "eyJpdiI6...",
"expires_at": "2026-07-14T15:02:11+09:00",
"order": {
"order_number": "20260714000012",
"order_status": "payment_complete"
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | 주문 없음 / 회원 주문 / 전화번호 불일치 / 비밀번호 오류 / 잠금 — 정보 노출 방지를 위해 모두 동일한 "주문을 찾을 수 없습니다" 로 처리 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
<!-- @generated:end -->
@@ -101,18 +123,145 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`GuestOrderResource` — 취소 후 갱신된 주문 상세)._
<a id="guest-order-resource"></a>
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| order_number | string | `20260714000012` | 주문번호 |
| order_status | string | `cancelled` | 주문 상태 (`pending_order`, `pending_payment`, `payment_complete`, `shipping_hold`, `preparing`, `shipping_ready`, `shipping`, `delivered`, `confirmed`, `cancelled`) |
| order_status_label | string\|null | `주문취소` | 주문 상태의 다국어 라벨 |
| order_status_variant | string\|null | `danger` | 주문 상태 뱃지 표시용 variant |
| is_partially_cancelled | boolean | `false` | 일부 옵션만 취소된 주문인지 여부 (별도 상태가 아닌 파생 플래그, 보조 뱃지용) |
| subtotal_amount | number | `12000` | 상품 합계 금액 (주문 시점 통화 기준) |
| subtotal_amount_formatted | string | `12,000원` | 상품 합계 금액 표기 문자열 |
| total_discount_amount | number | `0` | 총 할인 금액 |
| total_discount_amount_formatted | string | `0원` | 총 할인 금액 표기 문자열 |
| total_shipping_amount | number | `3000` | 총 배송비 |
| total_shipping_amount_formatted | string | `3,000원` | 총 배송비 표기 문자열 |
| total_amount | number | `15000` | 주문 총액 |
| total_amount_formatted | string | `15,000원` | 주문 총액 표기 문자열 |
| total_paid_amount | number | `15000` | 실제 결제 금액 |
| total_paid_amount_formatted | string | `15,000원` | 실제 결제 금액 표기 문자열 |
| total_cancelled_amount | number | `15000` | 누적 취소 금액 |
| total_cancelled_amount_formatted | string | `15,000원` | 누적 취소 금액 표기 문자열 |
| total_refunded_amount | number | `15000` | 누적 PG 환불 금액 |
| total_refunded_amount_formatted | string | `15,000원` | 누적 PG 환불 금액 표기 문자열 |
| total_refunded_points_amount | number | `0` | 누적 마일리지 환불 금액 |
| total_refunded_points_amount_formatted | string | `0원` | 누적 마일리지 환불 금액 표기 문자열 |
| total_points_used_amount | number | `0` | 사용한 마일리지 금액 |
| total_points_used_amount_formatted | string | `0원` | 사용 마일리지 표기 문자열 |
| total_deposit_used_amount | number | `0` | 사용한 예치금 금액 |
| total_deposit_used_amount_formatted | string | `0원` | 사용 예치금 표기 문자열 |
| total_earned_points_amount | number | `0` | 적립 예정/확정 마일리지 금액 |
| total_earned_points_amount_formatted | string | `0원` | 적립 마일리지 표기 문자열 |
| mc_subtotal_amount | object\|null | `{"KRW":"12,000원","USD":"$8.60"}` | 상품 합계의 다중 통화 스냅샷 (통화코드 → 표기 문자열) |
| mc_total_discount_amount | object\|null | — | 총 할인의 다중 통화 스냅샷 |
| mc_total_shipping_amount | object\|null | — | 총 배송비의 다중 통화 스냅샷 |
| mc_total_amount | object\|null | — | 주문 총액의 다중 통화 스냅샷 |
| mc_total_points_used_amount | object\|null | — | 사용 마일리지의 다중 통화 스냅샷 |
| mc_total_deposit_used_amount | object\|null | — | 사용 예치금의 다중 통화 스냅샷 |
| item_count | integer | `1` | 주문 품목 종류 수 |
| total_quantity | integer | `2` | 주문 총 수량 (옵션 수량 합) |
| ordered_at | string\|null | `2026-07-14T14:32:11+09:00` | 주문 일시 (ISO 8601) |
| ordered_at_formatted | string\|null | `2026-07-14 14:32` | 주문 일시 (사용자 타임존 표기) |
| paid_at | string\|null | `2026-07-14T14:33:02+09:00` | 결제 완료 일시 (ISO 8601) |
| paid_at_formatted | string\|null | `2026-07-14 14:33` | 결제 완료 일시 (사용자 타임존 표기) |
| confirmed_at | string\|null | `null` | 구매확정 일시 (ISO 8601) |
| confirmed_at_formatted | string\|null | `null` | 구매확정 일시 (사용자 타임존 표기) |
| cancelled_at | string\|null | `2026-07-14T15:01:44+09:00` | 취소 일시 (ISO 8601) |
| cancelled_at_formatted | string\|null | `2026-07-14 15:01` | 취소 일시 (사용자 타임존 표기) |
| orderer_name | string\|null | `홍길동` | 주문자 이름 |
| orderer_phone | string\|null | `010-1234-5678` | 주문자 전화번호 |
| orderer_email | string\|null | `guest@example.com` | 주문자 이메일 |
| recipient_name | string\|null | `홍길동` | 수령인 이름 |
| recipient_phone | string\|null | `010-1234-5678` | 수령인 연락처 |
| recipient_zipcode | string\|null | `06234` | 배송지 우편번호 |
| recipient_address | string\|null | `서울특별시 강남구 테헤란로 1` | 배송지 기본 주소 |
| recipient_detail_address | string\|null | `101호` | 배송지 상세 주소 |
| delivery_memo | string\|null | `부재 시 경비실에 맡겨주세요` | 배송 메모 |
| delivery_memo_label | string\|null | `부재 시 경비실` | 배송 메모 프리셋 라벨 |
| options | array | — | 주문 옵션(품목) 목록 (`OrderOptionResource`, 관계 로드 시에만 포함) |
| shipping_address | object\|null | — | 배송지 상세 (`OrderAddressResource`) |
| payment | object\|null | — | 결제 정보 (`OrderPaymentResource`, 관계 로드 시에만 포함) |
| shippings | array | — | 배송 정보 목록 (`OrderShippingResource`, 관계 로드 시에만 포함) |
| cancels | array | — | 취소 이력 목록 (`OrderCancelResource` — 취소 사유·상세·취소일시) |
| abilities | object | `{"can_cancel": false}` | 주문 상태로 판정한 허용 액션 (회원 권한이 아닌 상태 기반). `can_cancel` 은 환경설정 `order_settings.cancellable_statuses` 기준 |
회원용 `OrderResource` 와 달리 관리자/내부 메모(`admin_memo`, `customer_memo`), 회원 정보(`user`, `user_id`), 내부 계산 스냅샷(`promotions_applied_snapshot` 등)은 노출하지 않습니다.
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "주문이 취소되었습니다.",
"data": {
"order_number": "20260714000012",
"order_status": "cancelled",
"order_status_label": "주문취소",
"order_status_variant": "danger",
"is_partially_cancelled": false,
"subtotal_amount": 12000,
"subtotal_amount_formatted": "12,000원",
"total_discount_amount": 0,
"total_discount_amount_formatted": "0원",
"total_shipping_amount": 3000,
"total_shipping_amount_formatted": "3,000원",
"total_amount": 15000,
"total_amount_formatted": "15,000원",
"total_paid_amount": 15000,
"total_paid_amount_formatted": "15,000원",
"total_cancelled_amount": 15000,
"total_cancelled_amount_formatted": "15,000원",
"total_refunded_amount": 15000,
"total_refunded_amount_formatted": "15,000원",
"total_refunded_points_amount": 0,
"total_refunded_points_amount_formatted": "0원",
"total_points_used_amount": 0,
"total_points_used_amount_formatted": "0원",
"total_deposit_used_amount": 0,
"total_deposit_used_amount_formatted": "0원",
"total_earned_points_amount": 0,
"total_earned_points_amount_formatted": "0원",
"item_count": 1,
"total_quantity": 2,
"ordered_at": "2026-07-14T14:32:11+09:00",
"ordered_at_formatted": "2026-07-14 14:32",
"paid_at": "2026-07-14T14:33:02+09:00",
"paid_at_formatted": "2026-07-14 14:33",
"confirmed_at": null,
"confirmed_at_formatted": null,
"cancelled_at": "2026-07-14T15:01:44+09:00",
"cancelled_at_formatted": "2026-07-14 15:01",
"orderer_name": "홍길동",
"orderer_phone": "010-1234-5678",
"orderer_email": "guest@example.com",
"recipient_name": "홍길동",
"recipient_phone": "010-1234-5678",
"recipient_zipcode": "06234",
"recipient_address": "서울특별시 강남구 테헤란로 1",
"recipient_detail_address": "101호",
"delivery_memo": null,
"delivery_memo_label": null,
"options": [],
"shipping_address": {},
"shippings": [],
"cancels": [],
"abilities": {
"can_cancel": false
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 404 | Not Found | `X-Guest-Order-Token` 누락·만료·주문번호 불일치 (토큰이 지목한 주문 외에는 접근 불가) |
| 422 | Unprocessable Entity | 요청 파라미터 검증 위반 (`error.errors` 에 필드별 메시지), 또는 취소 처리 실패 (`주문 취소에 실패했습니다.`) |
<!-- @generated:end -->
@@ -329,18 +478,87 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`AdjustmentResult::toPreviewArray()` — 실제 취소는 수행하지 않는 환불 예상 계산 결과)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| refund_amount | number | `12000` | PG 환불 예상 금액 (음수면 추가결제 필요) |
| refund_points_amount | number | `0` | 마일리지 환불 예상 금액 |
| original_paid_amount | number | `15000` | 원 결제 금액 |
| recalculated_paid_amount | number | `3000` | 취소 후 재계산된 결제 금액 |
| shipping_difference | number | `0` | 배송비 차이 (양수: 환불 / 음수: 추가결제) |
| discount_difference | number | `0` | 할인 차이 (양수: 할인 감소분) |
| additional_payment_amount | number | `0` | 추가결제 필요 금액 (환불액이 음수일 때만 > 0) |
| cancelled_items | array | — | 취소 대상 항목 정보 (`order_option_id`, `cancel_quantity`, `cancel_amount`) |
| refund_priority | string | `pg_first` | 적용된 환불 배분 우선순위 (`pg_first`, `points_first`) |
| remaining_pg_balance | number | `0` | 환불 후 잔여 PG 결제 잔액 |
| remaining_points_balance | number | `0` | 환불 후 잔여 마일리지 잔액 |
| refund_total | number | `12000` | 총 환불 예상액 (PG 환불액 + 마일리지 환불액) |
| refund_formatted | object | — | 환불 총액·잔액의 표기 문자열 (base 통화 + 결제 통화 병기 — 취소 모달 표기 SSoT) |
| restored_coupons | array | — | 취소로 복원되는 쿠폰 정보 (`coupon_name`, `discount_amount`) |
| shipping_details | array | — | 배송비 정책별 상세 (`policy_name`, `base_difference`, `extra_difference`, `total_difference`) |
| mc_refund_amount | object\|null | — | PG 환불 금액의 다중 통화 표기 (통화코드 → 금액) |
| mc_refund_points_amount | object\|null | — | 마일리지 환불 금액의 다중 통화 표기 |
| mc_refund_shipping_amount | object\|null | — | 배송비 환불 금액의 다중 통화 표기 |
| original_snapshot | object | — | 재계산 전 주문 금액 스냅샷 |
| recalculated_snapshot | object | — | 재계산 후 주문 금액 스냅샷 |
| mc_original_snapshot | object\|null | — | 재계산 전 다중 통화 스냅샷 |
| mc_recalculated_snapshot | object\|null | — | 재계산 후 다중 통화 스냅샷 |
| original_coupons | array | — | 원 주문에 적용된 쿠폰 상세 (`name`, `target_type`, `discount_amount`) |
| recalculated_coupons | array | — | 재계산 후 유지되는 쿠폰 상세 |
| cancel_blocked | boolean | `false` | 취소 차단 여부 (부분취소로 쿠폰 조건이 깨져 추가결제가 필요해지는 경우 true) |
| cancel_blocked_reason | string\|null | `null` | 차단 사유 메시지 (차단이 아니면 `null`) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "환불 예상금액을 조회했습니다.",
"data": {
"refund_amount": 12000,
"refund_points_amount": 0,
"original_paid_amount": 15000,
"recalculated_paid_amount": 3000,
"shipping_difference": 0,
"discount_difference": 0,
"additional_payment_amount": 0,
"cancelled_items": [
{
"order_option_id": 34,
"cancel_quantity": 2,
"cancel_amount": 12000
}
],
"refund_priority": "pg_first",
"remaining_pg_balance": 3000,
"remaining_points_balance": 0,
"refund_total": 12000,
"refund_formatted": {},
"restored_coupons": [],
"shipping_details": [],
"mc_refund_amount": null,
"mc_refund_points_amount": null,
"mc_refund_shipping_amount": null,
"original_snapshot": {},
"recalculated_snapshot": {},
"mc_original_snapshot": null,
"mc_recalculated_snapshot": null,
"original_coupons": [],
"recalculated_coupons": [],
"cancel_blocked": false,
"cancel_blocked_reason": null
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | `X-Guest-Order-Token` 누락·만료·주문번호 불일치 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 환불 예상금액 계산 실패 (`환불 예상금액 조회에 실패했습니다.`) |
<!-- @generated:end -->
@@ -370,17 +588,51 @@ Accept: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`GuestOrderResource` — 구매확정 후 갱신된 주문 상세). 필드 구성은 [비회원 주문 취소 응답 필드 표](#guest-order-resource)와 동일합니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| order_number | string | `20260714000012` | 주문번호 |
| order_status | string | `confirmed` | 구매확정 반영된 주문 상태 |
| order_status_label | string\|null | `구매확정` | 주문 상태의 다국어 라벨 |
| confirmed_at | string\|null | `2026-07-14T16:10:03+09:00` | 구매확정 일시 (ISO 8601) |
| options | array | — | 주문 옵션 목록 — 확정된 옵션의 상태가 갱신되어 포함 |
| abilities | object | `{"can_cancel": false}` | 주문 상태 기반 허용 액션 |
| _(그 외)_ | — | — | 금액·일시·주문자/수취인·배송지·결제·배송·취소이력 필드는 [비회원 주문 취소 응답 필드 표](#guest-order-resource)와 동일 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "구매확정이 완료되었습니다.",
"data": {
"order_number": "20260714000012",
"order_status": "confirmed",
"order_status_label": "구매확정",
"order_status_variant": "success",
"is_partially_cancelled": false,
"total_amount": 15000,
"total_amount_formatted": "15,000원",
"confirmed_at": "2026-07-14T16:10:03+09:00",
"confirmed_at_formatted": "2026-07-14 16:10",
"options": [],
"shipping_address": {},
"shippings": [],
"cancels": [],
"abilities": {
"can_cancel": false
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | `X-Guest-Order-Token` 누락·만료·주문번호 불일치, 또는 `optionId` 가 해당 주문에 속하지 않는 경우 |
| 422 | Unprocessable Entity | 구매확정 불가 상태 (배송 미완료 등 — `현재 상태에서는 구매확정할 수 없습니다.`) |
<!-- @generated:end -->
@@ -439,18 +691,54 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`GuestOrderResource` — 배송지 수정 후 갱신된 주문 상세). 필드 구성은 [비회원 주문 취소 응답 필드 표](#guest-order-resource)와 동일합니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| order_number | string | `20260714000012` | 주문번호 |
| recipient_name | string\|null | `홍길동` | 수정 반영된 수령인 이름 |
| recipient_phone | string\|null | `010-1234-5678` | 수정 반영된 수령인 연락처 |
| recipient_zipcode | string\|null | `06234` | 수정 반영된 우편번호 |
| recipient_address | string\|null | `서울특별시 강남구 테헤란로 1` | 수정 반영된 기본 주소 |
| recipient_detail_address | string\|null | `101호` | 수정 반영된 상세 주소 |
| delivery_memo | string\|null | `부재 시 경비실에 맡겨주세요` | 수정 반영된 배송 메모 |
| shipping_address | object\|null | — | 배송지 상세 (`OrderAddressResource` — 국제 주소 필드 포함) |
| _(그 외)_ | — | — | 상태·금액·일시·옵션·결제·배송·취소이력·abilities 필드는 [비회원 주문 취소 응답 필드 표](#guest-order-resource)와 동일 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "배송지가 변경되었습니다.",
"data": {
"order_number": "20260714000012",
"order_status": "payment_complete",
"order_status_label": "결제완료",
"recipient_name": "홍길동",
"recipient_phone": "010-1234-5678",
"recipient_zipcode": "06234",
"recipient_address": "서울특별시 강남구 테헤란로 1",
"recipient_detail_address": "101호",
"delivery_memo": "부재 시 경비실에 맡겨주세요",
"delivery_memo_label": null,
"shipping_address": {},
"options": [],
"shippings": [],
"cancels": [],
"abilities": {
"can_cancel": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 404 | Not Found | `X-Guest-Order-Token` 누락·만료·주문번호 불일치 |
| 422 | Unprocessable Entity | 요청 파라미터 검증 위반 (`error.errors` 에 필드별 메시지), 또는 배송이 이미 시작되어 수정 불가 (`배송 전 상태에서만 배송지를 변경할 수 있습니다.`) |
<!-- @generated:end -->
@@ -40,11 +40,23 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted | boolean | `true` | 삭제 성공 여부 (컨트롤러가 상수 `true` 로 반환) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "문의가 삭제되었습니다.",
"data": {
"deleted": true
}
}
```
**에러 응답**
@@ -53,6 +65,8 @@ Authorization: Bearer {YOUR_TOKEN}
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.delete`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::deleteInquiry()` 의 `RuntimeException` |
| 500 | Internal Server Error | 삭제 처리 중 예기치 못한 예외 (`문의 삭제에 실패했습니다.`) |
<!-- @generated:end -->
@@ -82,11 +96,23 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted | boolean | `true` | 답변 삭제 성공 여부 (컨트롤러가 상수 `true` 로 반환) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "답변이 삭제되었습니다.",
"data": {
"deleted": true
}
}
```
**에러 응답**
@@ -95,6 +121,8 @@ Authorization: Bearer {YOUR_TOKEN}
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::deleteReply()` 의 `RuntimeException` |
| 500 | Internal Server Error | 답변 삭제 처리 중 예기치 못한 예외 (`답변 삭제에 실패했습니다.`) |
<!-- @generated:end -->
@@ -130,11 +158,25 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 답변이 등록된 문의(피벗 `product_inquiries`)의 기본 키 |
| is_answered | boolean | `true` | 답변 등록 후의 문의 답변 완료 여부 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "답변이 등록되었습니다.",
"data": {
"id": 1,
"is_answered": true
}
}
```
**에러 응답**
@@ -143,7 +185,8 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 답변 게시글 생성에 실패한 경우(`답변 등록에 실패했습니다.`) — `ProductInquiryService::createReply()` 의 `RuntimeException` |
| 500 | Internal Server Error | 답변 등록 처리 중 예기치 못한 예외 (`답변 등록에 실패했습니다.`) |
<!-- @generated:end -->
@@ -179,11 +222,23 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 답변을 수정한 문의(피벗 `product_inquiries`)의 기본 키 (요청 path 의 `inquiryId` 를 그대로 반환) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "답변이 수정되었습니다.",
"data": {
"id": 1
}
}
```
**에러 응답**
@@ -192,7 +247,8 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::updateReply()` 의 `RuntimeException` |
| 500 | Internal Server Error | 답변 수정 처리 중 예기치 못한 예외 (`답변 수정에 실패했습니다.`) |
<!-- @generated:end -->
@@ -372,18 +428,32 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted | boolean | `true` | 삭제 성공 여부 (컨트롤러가 상수 `true` 로 반환) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "문의가 삭제되었습니다.",
"data": {
"deleted": true
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 403 | Forbidden | 문의 작성자(`user_id`)가 로그인 사용자와 다른 경우 (`해당 문의에 대한 권한이 없습니다.`) |
| 404 | Not Found | 해당 ID 의 문의가 존재하지 않는 경우 (`문의를 찾을 수 없습니다.`) |
| 500 | Internal Server Error | 삭제 처리 중 예기치 못한 예외 (`문의 삭제에 실패했습니다.`) — 문의 게시판 미설정 등 서비스 `RuntimeException` 포함 |
<!-- @generated:end -->
@@ -401,10 +471,10 @@ Authorization: Bearer {YOUR_TOKEN}
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| inquiryId | path | string | 예 | — | 대상 inquiry의 식별자 |
| title | body | string | 아니오 | min 2, max 200 | 제목 |
| title | body | string | 아니오 | — | 문의 제목 (연동 게시판 Post 의 제목으로 저장. 게시판 설정에 따라 min/max 길이 규칙이 Filter 훅으로 추가됨) |
| category | body | string | 아니오 | — | 문의 분류 (게시판 설정 기반 유형 슬러그, 연동 게시판 Post 로 저장) |
| content | body | string | 예 | min 10, max 10000 | 본문 내용 |
| is_secret | body | boolean | 아니오 | — | secret 여부 |
| content | body | string | 예 | — | 문의 본문 (필수. 게시판 설정에 따라 min/max 길이 규칙이 Filter 훅으로 추가됨) |
| is_secret | body | boolean | 아니오 | — | 비밀글 여부 (게시판 비밀글 모드 설정에 따라 적용) |
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.inquiry.update_validation_rules`).
@@ -427,19 +497,33 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 수정한 문의(피벗 `product_inquiries`)의 기본 키 (요청 path 의 `inquiryId` 를 그대로 반환) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "문의가 수정되었습니다.",
"data": {
"id": 1
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 403 | Forbidden | 문의 작성자(`user_id`)가 로그인 사용자와 다른 경우 (`해당 문의에 대한 권한이 없습니다.`) |
| 404 | Not Found | 해당 ID 의 문의가 존재하지 않는 경우 (`문의를 찾을 수 없습니다.`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::updateInquiry()` 의 `RuntimeException` |
| 500 | Internal Server Error | 수정 처리 중 예기치 못한 예외 (`문의 수정에 실패했습니다.`) |
<!-- @generated:end -->
@@ -469,18 +553,33 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted | boolean | `true` | 답변 삭제 성공 여부 (컨트롤러가 상수 `true` 로 반환) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "답변이 삭제되었습니다.",
"data": {
"deleted": true
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 답변 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 (`해당 문의에 대한 권한이 없습니다.`) |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::deleteReply()` 의 `RuntimeException` |
| 500 | Internal Server Error | 답변 삭제 처리 중 예기치 못한 예외 (`답변 삭제에 실패했습니다.`) |
<!-- @generated:end -->
@@ -516,19 +615,35 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 답변이 등록된 문의(피벗 `product_inquiries`)의 기본 키 |
| is_answered | boolean | `true` | 답변 등록 후의 문의 답변 완료 여부 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "답변이 등록되었습니다.",
"data": {
"id": 1,
"is_answered": true
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 답변 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 (`해당 문의에 대한 권한이 없습니다.`) |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 답변 게시글 생성에 실패한 경우(`답변 등록에 실패했습니다.`) — `ProductInquiryService::createReply()` 의 `RuntimeException` |
| 500 | Internal Server Error | 답변 등록 처리 중 예기치 못한 예외 (`답변 등록에 실패했습니다.`) |
<!-- @generated:end -->
@@ -564,19 +679,33 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 답변을 수정한 문의(피벗 `product_inquiries`)의 기본 키 (요청 path 의 `inquiryId` 를 그대로 반환) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "답변이 수정되었습니다.",
"data": {
"id": 1
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 답변 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 (`해당 문의에 대한 권한이 없습니다.`) |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::updateReply()` 의 `RuntimeException` |
| 500 | Internal Server Error | 답변 수정 처리 중 예기치 못한 예외 (`답변 수정에 실패했습니다.`) |
<!-- @generated:end -->
@@ -268,11 +268,94 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (성공 시 HTTP 201). 생성 직후 모델을 그대로 직렬화하므로 관계 기반 필드(`user_name`·`user_uuid`·`granted_by_name`·`granted_by_uuid`·`order_number`)는 eager load 되지 않아 응답에서 생략됩니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `529` | 생성된 거래의 기본 키 |
| user_id | integer | `166` | 거래 대상 회원 ID (요청의 `user_id` uuid 를 내부 ID 로 해석한 값) |
| currency | string | `KRW` | 거래 기록 통화 코드 (요청 `currency`) |
| type | string | `admin_earn` | 거래 유형 (`action=earn` → `admin_earn`, `action=deduct` → `admin_deduct`) |
| type_label | string | `관리자 지급` | `type` 값의 사람이 읽는 라벨 |
| admin_badge_group | string | `amber` | 관리자 내역 화면 배지 색상 그룹 (수동/회수계=amber) |
| user_display_category | string | `adjust` | 회원 마이페이지 표시용 4분류 (수동 지급/차감은 adjust) |
| amount | integer | `1000` | 거래 금액 (지급=양수, 차감=음수) |
| amount_formatted | string | `1,000원` | `amount` 의 표시용 포맷 문자열 |
| remaining_amount | integer | `1000` | 잔여 금액 (지급 시 amount 와 동일, 차감 거래는 0) |
| remaining_amount_formatted | string | `1,000원` | `remaining_amount` 의 표시용 포맷 문자열 |
| balance_after | integer | `1000` | 거래 직후 해당 통화 잔액 스냅샷 |
| order_id | null | `null` | 수동 거래이므로 연결 주문 없음 |
| order_option_id | null | `null` | 수동 거래이므로 연결 주문 옵션 없음 |
| order_cancel_id | null | `null` | 수동 거래이므로 연결 주문취소 없음 |
| source_transaction_id | null | `null` | 수동 거래이므로 원본 거래 없음 |
| granted_by | integer | `1` | 처리한 관리자 user ID (인증 사용자) |
| description | string | `관리자 마일리지 지급 (1,000)` | 설명 (요청 `description` 미지정 시 활동로그 기본 문구) |
| memo | string \| null | `수기 보상` | 관리자 메모 (요청 `memo`) |
| expires_at | string \| null | `2027-07-02T15:29:30+00:00` | 만료 일시 (지급 시 정책 기본 또는 `expires_at`, 차감 거래는 null) |
| expires_at_formatted | string \| null | `2027-07-03 00:29:30` | `expires_at` 의 표시용 포맷 문자열 |
| expires_at_date | string \| null | `2027-07-03` | `expires_at` 의 사이트 타임존 기준 날짜 |
| expired_at | null | `null` | 소멸 일시 (신규 거래는 항상 null) |
| expired_at_formatted | null | `null` | `expired_at` 의 표시용 포맷 문자열 |
| created_at | string | `2026-07-07T05:47:31+00:00` | 생성 일시 |
| created_at_formatted | string | `2026-07-07 14:47:31` | `created_at` 의 표시용 포맷 문자열 |
| created_at_date | string | `2026-07-07` | `created_at` 의 사이트 타임존 기준 날짜 |
| is_earning | boolean | `true` | 적립계 여부 (`admin_earn`=true, `admin_deduct`=false) |
| can_edit_expiry | boolean | `true` | 만료일 편집 가능 여부 (적립계 + 미소멸 + 잔여>0) |
| expired_amount | integer | `0` | 이 적립 lot 을 source 로 소멸된 금액 합계 (단건 응답은 집계 미주입 → 0 폴백) |
| expired_amount_formatted | string | `0원` | `expired_amount` 의 표시용 포맷 문자열 |
| expiry_state | string | `active` | 적립건 소멸 상태 (신규 거래는 active) |
| abilities | object | `{"can_manage":true,"can_edit":true}` | 현재 사용자가 이 거래에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "마일리지 거래가 처리되었습니다.",
"data": {
"id": 529,
"user_id": 166,
"currency": "KRW",
"type": "admin_earn",
"type_label": "관리자 지급",
"admin_badge_group": "amber",
"user_display_category": "adjust",
"amount": 1000,
"amount_formatted": "1,000원",
"remaining_amount": 1000,
"remaining_amount_formatted": "1,000원",
"balance_after": 1000,
"order_id": null,
"order_option_id": null,
"order_cancel_id": null,
"source_transaction_id": null,
"granted_by": 1,
"description": "관리자 마일리지 지급 (1,000)",
"memo": "수기 보상",
"expires_at": "2027-07-02T15:29:30+00:00",
"expires_at_formatted": "2027-07-03 00:29:30",
"expires_at_date": "2027-07-03",
"expired_at": null,
"expired_at_formatted": null,
"created_at": "2026-07-07T05:47:31+00:00",
"created_at_formatted": "2026-07-07 14:47:31",
"created_at_date": "2026-07-07",
"is_earning": true,
"can_edit_expiry": true,
"expired_amount": 0,
"expired_amount_formatted": "0원",
"expiry_state": "active",
"abilities": {
"can_manage": true,
"can_edit": true
}
}
}
```
**에러 응답**
@@ -280,7 +363,7 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.mileage.manage`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반했거나, 도메인 규칙 위반(`MileageValidationException` — 예: 차감액이 잔액 초과) 시. 후자는 `message`=`마일리지 처리에 실패했습니다.` + `error.errors.general[]` 에 사유 문장 |
<!-- @generated:end -->
@@ -321,11 +404,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| extended_count | integer | `2` | 실제로 만료일이 연장된 적립 lot 수 (잔여 없는/대상 외 lot 은 제외되므로 요청한 `lot_ids` 수보다 작을 수 있음) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "마일리지 유효기간이 연장되었습니다.",
"data": {
"extended_count": 2
}
}
```
**에러 응답**
@@ -371,11 +470,94 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (수정된 거래 전체). 관계 기반 필드(`user_name`·`user_uuid`·`granted_by_name`·`granted_by_uuid`·`order_number`)는 eager load 되지 않아 응답에서 생략됩니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `528` | 수정한 거래의 기본 키 (path `{id}`) |
| user_id | integer | `166` | 거래 대상 회원 ID |
| currency | string | `KRW` | 거래 기록 통화 코드 |
| type | string | `admin_earn` | 거래 유형 (적립계만 수정 가능 — purchase_earn·admin_earn) |
| type_label | string | `관리자 지급` | `type` 값의 사람이 읽는 라벨 |
| admin_badge_group | string | `amber` | 관리자 내역 화면 배지 색상 그룹 |
| user_display_category | string | `adjust` | 회원 마이페이지 표시용 4분류 |
| amount | integer | `1000` | 거래 금액 (원장 불변 — 수정 불가) |
| amount_formatted | string | `1,000원` | `amount` 의 표시용 포맷 문자열 |
| remaining_amount | integer | `1000` | 잔여 금액 (FIFO 차감으로 소진) |
| remaining_amount_formatted | string | `1,000원` | `remaining_amount` 의 표시용 포맷 문자열 |
| balance_after | integer | `1000` | 거래 직후 잔액 스냅샷 |
| order_id | integer \| null | `null` | 연결 주문 ID |
| order_option_id | integer \| null | `null` | 연결 주문 옵션 ID |
| order_cancel_id | integer \| null | `null` | 연결 주문취소 ID |
| source_transaction_id | integer \| null | `null` | 원본 거래 ID |
| granted_by | integer \| null | `1` | 부여 주체 user ID (NULL=시스템 자동) |
| description | string \| null | `관리자 마일리지 지급 (1,000)` | 설명 |
| memo | string \| null | `사유 보정` | 갱신된 관리자 메모 (요청에 `memo` 를 포함한 경우 반영, 빈 값이면 null) |
| expires_at | string \| null | `2027-08-01T15:00:00+00:00` | 갱신된 만료 일시 (요청에 `expires_at` 을 포함한 경우 반영) |
| expires_at_formatted | string \| null | `2027-08-02 00:00:00` | `expires_at` 의 표시용 포맷 문자열 |
| expires_at_date | string \| null | `2027-08-02` | `expires_at` 의 사이트 타임존 기준 날짜 |
| expired_at | null | `null` | 소멸 일시 (소멸된 lot 은 만료일 수정 불가) |
| expired_at_formatted | null | `null` | `expired_at` 의 표시용 포맷 문자열 |
| created_at | string | `2026-07-07T05:47:31+00:00` | 생성 일시 |
| created_at_formatted | string | `2026-07-07 14:47:31` | `created_at` 의 표시용 포맷 문자열 |
| created_at_date | string | `2026-07-07` | `created_at` 의 사이트 타임존 기준 날짜 |
| is_earning | boolean | `true` | 적립계 여부 (수정 대상은 항상 true) |
| can_edit_expiry | boolean | `true` | 만료일 편집 가능 여부 (적립계 + 미소멸 + 잔여>0) |
| expired_amount | integer | `0` | 소멸 금액 합계 (단건 응답은 집계 미주입 → 0 폴백) |
| expired_amount_formatted | string | `0원` | `expired_amount` 의 표시용 포맷 문자열 |
| expiry_state | string | `active` | 적립건 소멸 상태 (active·partial_expired·fully_expired) |
| abilities | object | `{"can_manage":true,"can_edit":true}` | 현재 사용자가 이 거래에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "마일리지 거래가 수정되었습니다.",
"data": {
"id": 528,
"user_id": 166,
"currency": "KRW",
"type": "admin_earn",
"type_label": "관리자 지급",
"admin_badge_group": "amber",
"user_display_category": "adjust",
"amount": 1000,
"amount_formatted": "1,000원",
"remaining_amount": 1000,
"remaining_amount_formatted": "1,000원",
"balance_after": 1000,
"order_id": null,
"order_option_id": null,
"order_cancel_id": null,
"source_transaction_id": null,
"granted_by": 1,
"description": "관리자 마일리지 지급 (1,000)",
"memo": "사유 보정",
"expires_at": "2027-08-01T15:00:00+00:00",
"expires_at_formatted": "2027-08-02 00:00:00",
"expires_at_date": "2027-08-02",
"expired_at": null,
"expired_at_formatted": null,
"created_at": "2026-07-07T05:47:31+00:00",
"created_at_formatted": "2026-07-07 14:47:31",
"created_at_date": "2026-07-07",
"is_earning": true,
"can_edit_expiry": true,
"expired_amount": 0,
"expired_amount_formatted": "0원",
"expiry_state": "active",
"abilities": {
"can_manage": true,
"can_edit": true
}
}
}
```
**에러 응답**
@@ -384,7 +566,7 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.mileage.manage`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반했거나, 도메인 규칙 위반(`MileageValidationException`) 시. `message`=`마일리지 처리에 실패했습니다.` + `error.errors.general[]` 에 사유 문장 — 거래 없음(`마일리지 거래를 찾을 수 없습니다.`), 적립계 아님(`적립 거래만 수정할 수 있습니다.`), 소멸/전액 사용된 적립건(`이미 소멸되었거나 모두 사용된 적립건은 유효기간을 변경할 수 없습니다.`), 만료일이 적립일시 이전(`유효기간은 적립일시보다 이전일 수 없습니다.`) |
<!-- @generated:end -->
@@ -414,7 +596,44 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_목록 응답: `data.data[]` 배열 항목의 필드. 페이지네이션 없는 컬렉션이라 `data.pagination` 은 없고, 컬렉션 레벨 `data.abilities`(`can_manage`)와 통화 필터 후보 `data.currencies`(이 엔드포인트는 주입하지 않아 항상 `[]`)가 함께 내려옵니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| number | integer | `1` | 목록에서의 순번 (HasRowNumber 파생) |
| id | integer | `531` | 연결 거래의 기본 키 |
| user_id | integer | `166` | 거래 대상 회원 ID |
| currency | string | `KRW` | 거래 기록 통화 코드 |
| type | string | `order_use` | 거래 유형 (적립건이면 이를 FIFO 로 소비한 차감 거래, 차감/복원건이면 원본 적립·취소 연결 거래) |
| type_label | string | `주문 사용` | `type` 값의 사람이 읽는 라벨 |
| admin_badge_group | string | `blue` | 관리자 내역 화면 배지 색상 그룹 |
| user_display_category | string | `use` | 회원 마이페이지 표시용 4분류 |
| amount | integer | `-500` | 거래 금액 (양수=적립, 음수=차감) |
| amount_formatted | string | `-500원` | `amount` 의 표시용 포맷 문자열 |
| remaining_amount | integer | `0` | 잔여 금액 (적립건만 양수) |
| remaining_amount_formatted | string | `0원` | `remaining_amount` 의 표시용 포맷 문자열 |
| balance_after | integer | `500` | 거래 직후 잔액 스냅샷 |
| order_id | integer \| null | `436` | 연결 주문 ID |
| order_option_id | integer \| null | `824` | 연결 주문 옵션 ID |
| order_cancel_id | integer \| null | `null` | 연결 주문취소 ID |
| source_transaction_id | integer \| null | `528` | 원본(source) 거래 ID — FIFO 소비된 적립 lot 또는 복원 원본 |
| granted_by | integer \| null | `null` | 부여 주체 user ID (NULL=시스템 자동) |
| description | string \| null | `주문 마일리지 사용` | 설명 |
| memo | string \| null | `null` | 관리자 메모 |
| expires_at | string \| null | `null` | 만료 일시 |
| expires_at_formatted | string \| null | `null` | `expires_at` 의 표시용 포맷 문자열 |
| expires_at_date | string \| null | `null` | `expires_at` 의 사이트 타임존 기준 날짜 |
| expired_at | string \| null | `null` | 소멸 일시 |
| expired_at_formatted | string \| null | `null` | `expired_at` 의 표시용 포맷 문자열 |
| created_at | string | `2026-07-07T05:47:31+00:00` | 생성 일시 |
| created_at_formatted | string | `2026-07-07 14:47:31` | `created_at` 의 표시용 포맷 문자열 |
| created_at_date | string | `2026-07-07` | `created_at` 의 사이트 타임존 기준 날짜 |
| is_earning | boolean | `false` | 적립계 여부 |
| can_edit_expiry | boolean | `false` | 만료일 편집 가능 여부 |
| expired_amount | integer | `0` | 소멸 금액 합계 (연결 거래 조회는 집계 미주입 → 0 폴백) |
| expired_amount_formatted | string | `0원` | `expired_amount` 의 표시용 포맷 문자열 |
| expiry_state | string | `active` | 적립건 소멸 상태 (적립계만 의미) |
| abilities | object | `{"can_manage":true,"can_edit":false}` | 현재 사용자가 이 거래에 수행 가능한 작업 불리언 맵 |
**응답 예시**
@@ -229,11 +229,29 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| max_usable | integer | `1000` | 해당 주문금액(`order_amount`)에 사용 가능한 최대 마일리지. 보유 잔액·최대 사용 한도(percent/fixed)·주문금액 상한 중 최솟값을 취한 뒤 사용 단위(`use_unit`)로 내림 보정한 값 (기본 통화 사용 규칙 미설정 시 `0`) |
| available | integer | `12910` | 현재 사용 가능한 마일리지 잔액 (`getBalance()` 의 `available` 값과 동일) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "사용 가능한 최대 마일리지를 조회했습니다.",
"data": {
"max_usable": 1000,
"available": 12910
}
}
```
**에러 응답**
@@ -59,11 +59,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductOptionService::bulkUpdatePriceByMixedIds()` 반환 배열)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `3` | 실제로 판매가가 변경된 옵션 건수 (`product_ids` 로 확장된 옵션 + `option_ids` 로 개별 선택된 옵션을 중복 제거한 대상 수) |
| requested_product_count | integer | `1` | 요청에 담긴 `product_ids` 배열의 원소 수 (옵션만 선택한 경우 `0`) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": ":count개 옵션의 가격이 수정되었습니다.",
"data": {
"updated_count": 3,
"requested_product_count": 1
}
}
```
> `message` 는 `sirsoft-ecommerce::messages.options.bulk_price_updated` 를 그대로 사용합니다. 컨트롤러가 `messageParams` 를 전달하지 않으므로 `:count` 플레이스홀더는 치환되지 않은 채 응답됩니다 (변경 건수는 `data.updated_count` 로 확인).
**에러 응답**
@@ -71,7 +87,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / Service 단계에서 `ValidationException` 발생 시 `옵션 가격 일괄 변경에 실패했습니다.` |
| 500 | Internal Server Error | 그 외 예외 발생 시 (`옵션 가격 일괄 변경에 실패했습니다.`) |
<!-- @generated:end -->
@@ -118,11 +135,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductOptionService::bulkUpdateStockByMixedIds()` 반환 배열)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `3` | 실제로 재고가 변경된 옵션 건수 (`product_ids` 로 확장된 옵션 + `option_ids` 로 개별 선택된 옵션을 중복 제거한 대상 수) |
| requested_product_count | integer | `1` | 요청에 담긴 `product_ids` 배열의 원소 수 (옵션만 선택한 경우 `0`) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": ":count개 옵션의 재고가 수정되었습니다.",
"data": {
"updated_count": 3,
"requested_product_count": 1
}
}
```
> `message` 는 `sirsoft-ecommerce::messages.options.bulk_stock_updated` 를 그대로 사용합니다. 컨트롤러가 `messageParams` 를 전달하지 않으므로 `:count` 플레이스홀더는 치환되지 않은 채 응답됩니다 (변경 건수는 `data.updated_count` 로 확인).
**에러 응답**
@@ -130,7 +163,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / Service 단계에서 `ValidationException` 발생 시 `옵션 재고 일괄 변경에 실패했습니다.` |
| 500 | Internal Server Error | 그 외 예외 발생 시 (`옵션 재고 일괄 변경에 실패했습니다.`) |
<!-- @generated:end -->
@@ -147,15 +181,15 @@ Content-Type: application/json
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) |
| ids | body | array | 예 | min 1 | 대상 옵션 식별자 배열. 각 원소는 `"productId-optionId"` 형식 문자열 (`regex:/^\d+-\d+$/`) |
| bulk_changes | body | array | 아니오 | — | 옵션 일괄 변경 조건 (`price_adjustment`/`stock_quantity` 각각 method+value, 설정된 필드가 개별 수정보다 우선 적용) |
| bulk_changes.price_adjustment | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| bulk_changes.price_adjustment.method | body | string | 아니오 | `set`, `add`, `percent` | <!-- TODO: 용도 --> |
| bulk_changes.price_adjustment.value | body | number | 아니오 | — | <!-- TODO: 용도 --> |
| bulk_changes.stock_quantity | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| bulk_changes.stock_quantity.method | body | string | 아니오 | `set`, `add`, `subtract` | <!-- TODO: 용도 --> |
| bulk_changes.stock_quantity.value | body | integer | 아니오 | min 0 | <!-- TODO: 용도 --> |
| items | body | array | 아니오 | — | 처리 대상 항목 배열 |
| bulk_changes.price_adjustment | body | array | 아니오 | — | 옵션 추가금액(`price_adjustment`) 일괄 변경 조건 객체. 존재 시 하위 `method`/`value` 필수 |
| bulk_changes.price_adjustment.method | body | string | 아니오 | `set`, `add`, `percent` | 추가금액 변경 방식 (set 지정값으로 설정 / add 기존값에 가산 / percent 기존값에 비율 적용) |
| bulk_changes.price_adjustment.value | body | number | 아니오 | — | 추가금액 변경 값 (`method` 에 따라 설정값·가감액·비율로 해석. 소수 통화 대응) |
| bulk_changes.stock_quantity | body | array | 아니오 | — | 옵션 재고수량(`stock_quantity`) 일괄 변경 조건 객체. 존재 시 하위 `method`/`value` 필수 |
| bulk_changes.stock_quantity.method | body | string | 아니오 | `set`, `add`, `subtract` | 재고 변경 방식 (set 특정 수량으로 설정 / add 증가 / subtract 감소) |
| bulk_changes.stock_quantity.value | body | integer | 아니오 | min 0 | 재고 변경 값 (`method` 에 따라 설정 수량·증감 수량으로 해석) |
| items | body | array | 아니오 | — | 개별 인라인 수정 항목 배열. 항목별 키: `product_id`(필수, 존재하는 상품), `option_id`(필수, 존재하는 옵션), `option_name`(로케일별 문자열 배열, 각 max 255), `sku`(max 100), `list_price`(min 0), `price_adjustment`, `stock_quantity`(min 0), `safe_stock_quantity`(min 0), `is_default`, `is_active`. `bulk_changes` 에 설정된 필드는 개별값보다 우선 적용되어 무시됨 |
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.option.bulk_update_validation_rules`).
@@ -193,11 +227,25 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductOptionService::bulkUpdate()` 반환 배열)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| options_updated | integer | `5` | 적용된 변경 횟수. 일괄 변경(`bulk_changes.price_adjustment`, `bulk_changes.stock_quantity`)은 각 조건마다 대상 옵션 수만큼 누적되고, 개별 인라인 수정(`items`)은 실제로 변경 필드가 남은 항목마다 1씩 누적됩니다 (대상 옵션이 없으면 `0`) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "sirsoft-ecommerce::messages.options.bulk_updated",
"data": {
"options_updated": 5
}
}
```
> 컨트롤러가 사용하는 메시지 키 `messages.options.bulk_updated` 는 모듈 lang(ko/en)에 정의되어 있지 않습니다(정의된 키는 `messages.options.bulk_update_success`). 따라서 `message` 에는 번역문 대신 키 문자열이 그대로 반환됩니다.
**에러 응답**
@@ -205,7 +253,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / Service 단계에서 `ValidationException` 발생 시 `옵션 일괄 업데이트에 실패했습니다.` |
| 500 | Internal Server Error | 그 외 예외 발생 시 (`옵션 일괄 업데이트에 실패했습니다.`) |
<!-- @generated:end -->
File diff suppressed because it is too large. Load diff
@@ -39,17 +39,96 @@ Accept: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._ `data` 는 Resource 가 아니라 `sirsoft-ecommerce.payment.get_client_config` **필터 훅의 반환 배열을 그대로** 담습니다. 따라서 키 구성은 설치된 PG 플러그인이 결정하며, 코어(모듈)는 어떤 키도 강제하지 않습니다. 아래는 번들 PG 플러그인 두 종의 실제 반환 키입니다.
공통 (모든 PG 플러그인이 공통으로 내려주는 키)
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| sdk_url | string | `https://js.tosspayments.com/v2/standard` | 프론트가 로드해야 할 PG 결제 SDK 스크립트 URL (테스트/운영 모드에 따라 달라질 수 있음) |
| callback_urls | object | `{"success":"/plugins/sirsoft-tosspayments/payment/success","fail":"/plugins/sirsoft-tosspayments/payment/fail"}` | 결제창 성공/실패/콜백 등에서 사용할 플러그인 콜백 경로 모음 (키 구성은 PG마다 다름) |
`tosspayments` (플러그인 `sirsoft-tosspayments`)
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| client_key | string | `test_ck_...` | 토스페이먼츠 SDK 초기화용 클라이언트 키 (테스트 모드면 test 키, 운영 모드면 live 키) |
| sdk_url | string | `https://js.tosspayments.com/v2/standard` | 토스페이먼츠 v2 표준 SDK URL |
| order_sheet_mode | boolean | `false` | 결제수단을 주문서(오더시트)에서 직접 선택하는 모드 여부. `false` 면 통합결제창 방식 |
| enabled_methods | array | `[]` | 오더시트 모드에서 활성화된 결제수단 목록. 결제창형(`order_sheet_mode: false`)이면 빈 배열 |
| enabled_methods[].id | string | `toss_card` | 플러그인 설정상의 결제수단 식별자 |
| enabled_methods[].method | string | `CARD` | 토스 SDK 에 전달할 결제수단 코드 |
| enabled_methods[].easy_pay_provider | string\|null | `null` | 간편결제일 때 토스 SDK 의 easyPay provider 값 (일반 결제수단이면 null) |
| enabled_methods[].core_payment_method | string | `card` | 이 결제수단에 대응하는 코어 결제수단 값 (프론트 하드코딩 제거용) |
| vbank | object | `{"valid_hours":24,"cash_receipt_type":""}` | 가상계좌 관련 설정 묶음 |
| vbank.valid_hours | integer | `24` | 가상계좌 입금 기한(시간) |
| vbank.cash_receipt_type | string | `""` | 가상계좌 현금영수증 발급 유형 (미설정이면 빈 문자열) |
| use_escrow | string | `off` | 에스크로 사용 설정값 (플러그인 설정 문자열 그대로) |
| callback_urls.success | string | `/plugins/sirsoft-tosspayments/payment/success` | 결제 성공 리다이렉트 경로 |
| callback_urls.fail | string | `/plugins/sirsoft-tosspayments/payment/fail` | 결제 실패 리다이렉트 경로 |
`kginicis` (플러그인 `sirsoft-pay_kginicis`)
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| mid | string | `INIpayTest` | 이니시스 상점 아이디(MID). 테스트/운영 및 에스크로 사용 여부에 따라 선택됨 |
| sdk_url | string | `https://stgstdpay.inicis.com/stdjs/INIStdPay.js` | 이니시스 표준결제 SDK URL (테스트: stgstdpay, 운영: stdpay) |
| callback_urls | object | — | 서명/종료보고/콜백/CBT/모바일 등 이니시스 연동 경로 모음 |
| callback_urls.signature | string | `/plugins/sirsoft-pay_kginicis/payment/signature` | PC 표준결제 서명 발급 경로 |
| callback_urls.close_report | string | `/plugins/sirsoft-pay_kginicis/payment/close-report` | 결제창 종료 보고 경로 |
| callback_urls.callback | string | `/plugins/sirsoft-pay_kginicis/payment/callback` | PC 표준결제 인증 결과 콜백 경로 |
| callback_urls.close | string | `/plugins/sirsoft-pay_kginicis/payment/close` | 결제창 닫기 처리 경로 |
| callback_urls.cbt_checkout_token | string | `/plugins/sirsoft-pay_kginicis/payment/cbt/checkout-token` | 해외결제(CBT) 체크아웃 토큰 발급 경로 |
| callback_urls.cbt_hash_data | string | `/plugins/sirsoft-pay_kginicis/payment/cbt/hash-data` | CBT 해시 데이터 생성 경로 |
| callback_urls.cbt_callback | string | `/plugins/sirsoft-pay_kginicis/payment/cbt/callback` | CBT 결제 결과 콜백 경로 |
| callback_urls.cbt_cvs_notify | string | `/plugins/sirsoft-pay_kginicis/payment/cbt/cvs-notify` | CBT 편의점 결제 입금 통보 경로 |
| callback_urls.cbt_auth_url | string | — | CBT 인증 요청 URL (테스트/운영 상수값) |
| callback_urls.mobile_signature | string | `/plugins/sirsoft-pay_kginicis/payment/mobile/signature` | 모바일 결제 서명 발급 경로 |
| callback_urls.mobile_callback | string | `/plugins/sirsoft-pay_kginicis/payment/mobile/callback` | 모바일 결제 결과 콜백 경로 |
| callback_urls.mobile_vbank_notify | string | `/plugins/sirsoft-pay_kginicis/payment/mobile/vbank-notify` | 모바일 가상계좌 입금 통보 경로 |
| japan_enabled | boolean | `false` | 일본 결제(해외결제) 사용 여부 설정값 |
| japan_restrict_jpy_payment_methods | boolean | `false` | JPY 결제 시 결제수단을 일본 전용으로 제한할지 여부 |
| japan_configured | boolean | `false` | 일본 결제에 필요한 설정(MID/키 등)이 모두 채워졌는지 여부 |
| standard_configured | boolean | `true` | PC 표준결제 설정이 모두 채워졌는지 여부 |
| mobile_configured | boolean | `true` | 모바일 결제 설정이 모두 채워졌는지 여부 |
| use_escrow | boolean | `false` | 에스크로 사용 여부 설정값 |
| japan_mid | string | — | 일본 결제용 상점 아이디 (테스트면 고정 테스트 MID, 운영이면 설정된 live MID) |
| cbt_extra_data | object | — | CBT 결제 요청에 부가로 실어보낼 데이터 (플러그인 설정 기반으로 구성) |
| use_credit_point | boolean | `false` | 신용카드 포인트 결제 사용 여부 |
| easy_pay_show_brand_button | boolean | `false` | 간편결제 브랜드 버튼 노출 여부 |
| easy_pay_enabled_methods | array | `[]` | 활성화된 국내 간편결제 수단 목록 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
`tosspayments` (결제창형, 테스트 모드) 기준:
```json
{
"success": true,
"message": "결제 클라이언트 설정을 조회했습니다.",
"data": {
"client_key": "test_ck_xxxxxxxxxxxxxxxxxxxxxxxx",
"sdk_url": "https://js.tosspayments.com/v2/standard",
"order_sheet_mode": false,
"enabled_methods": [],
"vbank": {
"valid_hours": 24,
"cash_receipt_type": ""
},
"use_escrow": "off",
"callback_urls": {
"success": "/plugins/sirsoft-tosspayments/payment/success",
"fail": "/plugins/sirsoft-tosspayments/payment/fail"
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 해당 `provider` 에 대해 `sirsoft-ecommerce.payment.get_client_config` 필터가 빈 설정을 반환한 경우 (PG 플러그인 미설치·미활성·미지원 provider). 메시지: `결제 제공자를 찾을 수 없습니다.` |
<!-- @generated:end -->
@@ -42,9 +42,18 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
_목록 응답: `data[]` 배열 항목의 필드 (페이지네이션 없음 — 현재 사용자·해당 화면의 프리셋 전체를 배열로 반환)._
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 프리셋 ID (기본 키) |
| target_screen | string | `products` | 대상 화면 (`products` / `orders` / `customers`) |
| name | string | `배송 대기 상품` | 프리셋 이름 (DB 컬럼 `preset_name`, 최대 100자) |
| conditions | object | `{"status": "active"}` | 저장된 검색 조건 JSON (필터 필드 → 값) |
| filters | object | `{"preset_id": 1, "filters[status]": "active"}` | 프론트엔드가 그대로 붙여 쓰는 쿼리 파라미터 형태 (`SearchPreset::toQueryParams()` 생성, 배열 값은 콤마 결합) |
| sort_order | integer | `0` | 정렬 순서 (작을수록 우선) |
| is_default | boolean | `false` | 기본 프리셋 여부 |
| created_at | string | `2026-07-14 10:20:30` | 생성 일시 (사용자 타임존 기준 `Y-m-d H:i:s`) |
**응답 예시**
@@ -56,10 +65,28 @@ HTTP/1.1 200
{
"success": true,
"message": "검색 프리셋 목록을 조회했습니다.",
"data": []
"data": [
{
"id": 1,
"target_screen": "products",
"name": "배송 대기 상품",
"conditions": {
"status": "active"
},
"filters": {
"preset_id": 1,
"filters[status]": "active"
},
"sort_order": 0,
"is_default": false,
"created_at": "2026-07-14 10:20:30"
}
]
}
```
> `message` 는 컨트롤러가 `sirsoft-ecommerce::messages.presets.fetch_success` 키를 넘기지만 모듈 lang 파일(`src/lang/ko/messages.php` 의 `presets` 배열)에 해당 키가 정의되어 있지 않아, 실측 응답에서 키 문자열이 그대로 노출됩니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
@@ -108,18 +135,53 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (생성된 프리셋 1건)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 생성된 프리셋 ID |
| target_screen | string | `products` | 대상 화면 (`products` / `orders` / `customers`, 미지정 시 `products`) |
| name | string | `배송 대기 상품` | 프리셋 이름 (DB 컬럼 `preset_name`) |
| conditions | object | `{"status": "active"}` | 저장된 검색 조건 |
| filters | object | `{"preset_id": 1, "filters[status]": "active"}` | 프론트엔드용 쿼리 파라미터 형태 |
| sort_order | integer | `0` | 정렬 순서 (생성 시 기본값 `0`) |
| is_default | boolean | `false` | 기본 프리셋 여부 (생성 시 기본값 `false`) |
| created_at | string | `2026-07-14 10:20:30` | 생성 일시 (사용자 타임존 기준 `Y-m-d H:i:s`) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "검색 프리셋이 저장되었습니다.",
"data": {
"id": 1,
"target_screen": "products",
"name": "배송 대기 상품",
"conditions": {
"status": "active"
},
"filters": {
"preset_id": 1,
"filters[status]": "active"
},
"sort_order": 0,
"is_default": false,
"created_at": "2026-07-14 10:20:30"
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 동일 사용자·동일 화면에 같은 이름의 프리셋이 이미 있으면 `name` 에 "동일한 이름의 프리셋이 이미 존재합니다." |
<!-- @generated:end -->
@@ -149,11 +211,27 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (삭제 결과 플래그만 반환하며, 삭제된 프리셋 본문은 반환하지 않습니다)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted | boolean | `true` | 삭제 완료 여부 (컨트롤러가 `['deleted' => true]` 를 고정 반환) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "검색 프리셋이 삭제되었습니다.",
"data": {
"deleted": true
}
}
```
**에러 응답**
@@ -161,6 +239,7 @@ Authorization: Bearer {YOUR_TOKEN}
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 500 | UnauthorizedPresetAccessException | 다른 사용자 소유의 프리셋을 삭제하려는 경우 (`SearchPresetService::delete()` 의 소유권 검사 — "프리셋(ID: :preset_id)에 대한 접근 권한이 없습니다."). 이 예외는 전용 status 나 `render()` 를 갖지 않아 기본 예외 처리로 500 이 됩니다 (테스트 하네스 `ModuleTestCase` 에서만 403 으로 매핑) |
<!-- @generated:end -->
@@ -204,11 +283,46 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (수정된 프리셋 1건)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 프리셋 ID |
| target_screen | string | `products` | 대상 화면 (수정 대상이 아님 — 생성 시 값 유지) |
| name | string | `배송 대기 상품 (수정)` | 수정된 프리셋 이름 (DB 컬럼 `preset_name`) |
| conditions | object | `{"status": "inactive"}` | 수정된 검색 조건 |
| filters | object | `{"preset_id": 1, "filters[status]": "inactive"}` | 프론트엔드용 쿼리 파라미터 형태 |
| sort_order | integer | `1` | 정렬 순서 (작을수록 우선) |
| is_default | boolean | `false` | 기본 프리셋 여부 |
| created_at | string | `2026-07-14 10:20:30` | 생성 일시 (사용자 타임존 기준 `Y-m-d H:i:s`) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "검색 프리셋이 수정되었습니다.",
"data": {
"id": 1,
"target_screen": "products",
"name": "배송 대기 상품 (수정)",
"conditions": {
"status": "inactive"
},
"filters": {
"preset_id": 1,
"filters[status]": "inactive"
},
"sort_order": 1,
"is_default": false,
"created_at": "2026-07-14 10:20:30"
}
}
```
**에러 응답**
@@ -216,7 +330,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 동일 사용자·동일 화면에 같은 이름의 다른 프리셋이 있으면 `name` 에 "동일한 이름의 프리셋이 이미 존재합니다." |
| 500 | UnauthorizedPresetAccessException | 다른 사용자 소유의 프리셋을 수정하려는 경우 (`SearchPresetService::update()` 의 소유권 검사). 전용 status/`render()` 가 없어 기본 예외 처리로 500 (테스트 하네스에서만 403 매핑) |
<!-- @generated:end -->
@@ -206,11 +206,62 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductCommonInfoResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 생성된 공통정보의 기본 키 (내부 식별자) |
| name | object | `{"ko":"API 문서 샘플 공통정보","en":"API Doc Sample Common Info"}` | 공통정보 이름 (로케일별 값 객체) |
| localized_name | string | `API 문서 샘플 공통정보` | `name` 의 현재 로케일 해석 값 |
| content | object\|array | `[]` | 안내 본문 내용 (로케일별 값 객체, 미입력 시 빈 배열) |
| localized_content | string | `` | `content` 의 현재 로케일 해석 값 |
| content_mode | string | `text` | 내용 표시 모드 (`text` 일반 텍스트 / `html` HTML, 기본값 `text`) |
| is_default | boolean | `false` | 신규 상품에 기본으로 사용할 공통정보인지 여부 |
| is_active | boolean | `true` | 활성 여부 (false 이면 상품에 노출되지 않음) |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| icon | string | `info-circle` | 아이콘 식별자 (목록 UI 표시용 고정값) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 |
| products_count | integer | `0` | 이 공통정보를 사용 중인 상품 수 (생성 직후 0) |
| language_count | integer | `0` | 값이 입력된 로케일 개수 |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (권한 맵 기반) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "공통정보가 생성되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "API 문서 샘플 공통정보",
"en": "API Doc Sample Common Info"
},
"localized_name": "API 문서 샘플 공통정보",
"content": [],
"localized_content": "",
"content_mode": "text",
"is_default": false,
"is_active": true,
"sort_order": 0,
"icon": "info-circle",
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 10:44:49",
"products_count": 0,
"language_count": 0,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -354,11 +405,64 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductCommonInfoResource` — 수정 반영 후 값)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 수정된 공통정보의 기본 키 (내부 식별자) |
| name | object | `{"ko":"API 문서 샘플 공통정보(수정)","en":"API Doc Sample Common Info (updated)"}` | 공통정보 이름 (로케일별 값 객체) |
| localized_name | string | `API 문서 샘플 공통정보(수정)` | `name` 의 현재 로케일 해석 값 |
| content | object\|array | `{"ko":"배송은 결제 후 1~3일 이내 출고됩니다."}` | 안내 본문 내용 (로케일별 값 객체) |
| localized_content | string | `배송은 결제 후 1~3일 이내 출고됩니다.` | `content` 의 현재 로케일 해석 값 |
| content_mode | string | `text` | 내용 표시 모드 (`text` 일반 텍스트 / `html` HTML, 기본값 `text`) |
| is_default | boolean | `false` | 신규 상품에 기본으로 사용할 공통정보인지 여부 |
| is_active | boolean | `true` | 활성 여부 (false 이면 상품에 노출되지 않음) |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| icon | string | `info-circle` | 아이콘 식별자 (목록 UI 표시용 고정값) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 15:00:26` | 최종 수정 일시 (이번 수정 시각) |
| products_count | integer | `0` | 이 공통정보를 사용 중인 상품 수 |
| language_count | integer | `1` | 값이 입력된 로케일 개수 |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (권한 맵 기반) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "공통정보가 수정되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "API 문서 샘플 공통정보(수정)",
"en": "API Doc Sample Common Info (updated)"
},
"localized_name": "API 문서 샘플 공통정보(수정)",
"content": {
"ko": "배송은 결제 후 1~3일 이내 출고됩니다."
},
"localized_content": "배송은 결제 후 1~3일 이내 출고됩니다.",
"content_mode": "text",
"is_default": false,
"is_active": true,
"sort_order": 0,
"icon": "info-circle",
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 15:00:26",
"products_count": 0,
"language_count": 1,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -27,7 +27,7 @@
| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 |
| --- | --- | --- | --- | --- | --- |
| hash | path | string | 예 | — | 대상 리소스의 해시 식별자 |
| hash | path | string | 예 | — | 서빙할 상품 이미지의 URL용 고유 해시 (`ecommerce_product_images.hash`) |
**요청 예시**
@@ -39,17 +39,41 @@ Accept: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 JSON 봉투(`data`)를 반환하지 않습니다. 성공 시 이미지 원본 파일의 바이너리 스트림(`StreamedResponse`)을 직접 반환합니다._
| 응답 헤더 | 값 | 용도/설명 |
| --- | --- | --- |
| Content-Type | `image/jpeg` (레코드의 `mime_type` 값 그대로 — `image/png` \| `image/webp` \| `image/gif` 등) | 저장된 이미지의 MIME 타입 |
| Content-Disposition | `attachment; filename="product-main.jpg"` (레코드의 `original_filename`) | 업로드 당시의 원본 파일명 |
| Cache-Control | `public, max-age=31536000` | 브라우저/CDN 1년 캐싱 (해시 기반 URL이므로 내용 변경 시 URL 자체가 바뀜) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
성공 시 JSON 이 아닌 이미지 바이너리가 스트리밍됩니다.
```http
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Disposition: attachment; filename="product-main.jpg"
Cache-Control: public, max-age=31536000
<binary image data>
```
실패(404) 시에만 JSON 봉투가 반환됩니다.
```json
{
"success": false,
"message": "상품 이미지를 찾을 수 없습니다."
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 레코드는 있으나 스토리지에 실제 파일이 없어 스트림 생성에 실패한 경우 (`ProductImageService::download()` null — 서버 로그에 `상품 이미지 스토리지에 없음` 기록) |
<!-- @generated:end -->
@@ -166,11 +166,50 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductLabelResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| name | object | `{"ko":"신상품","en":"New"}` | 라벨명 (다국어 필드 — 로케일별 값 객체) |
| color | string | `#4F46E5` | 라벨 색상 코드 (`#RRGGBB` 6자리 HEX) |
| is_active | boolean | `true` | 활성 여부 (미지정 시 모델 기본값) |
| sort_order | integer | `1` | 표시 정렬 순서 값 (작을수록 우선) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 (사용자 타임존 기준 문자열) |
| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 (사용자 타임존 기준 문자열) |
| assignments_count | integer | `0` | 이 라벨이 부여된 상품 수 (집계) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (권한 맵 기반) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "라벨이 등록되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "신상품",
"en": "New"
},
"color": "#4F46E5",
"is_active": true,
"sort_order": 1,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 10:44:49",
"assignments_count": 0,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -306,11 +345,50 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductLabelResource` — 수정 후 라벨)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| name | object | `{"ko":"베스트","en":"Best"}` | 라벨명 (다국어 필드 — 로케일별 값 객체) |
| color | string | `#4F46E5` | 라벨 색상 코드 (`#RRGGBB` 6자리 HEX) |
| is_active | boolean | `true` | 활성 여부 |
| sort_order | integer | `1` | 표시 정렬 순서 값 (작을수록 우선) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 (사용자 타임존 기준 문자열) |
| updated_at | string | `2026-07-08 15:00:27` | 최종 수정 일시 (수정 반영 후 값) |
| assignments_count | integer | `0` | 이 라벨이 부여된 상품 수 (집계) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (권한 맵 기반) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "라벨이 수정되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "베스트",
"en": "Best"
},
"color": "#4F46E5",
"is_active": true,
"sort_order": 1,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 15:00:27",
"assignments_count": 0,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -272,16 +272,67 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductNoticeTemplateResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `3` | 생성된 템플릿의 기본 키 (내부 식별자) |
| name | object | `{"ko":"의류 고시","en":"Clothing Notice"}` | 템플릿명 (로케일별 값 객체) |
| localized_name | string | `의류 고시` | `name` 의 현재 로케일 해석 값 |
| category | string\|null | `clothing` | 이 템플릿이 적용되는 품목 카테고리 식별자 (미지정 시 `null`) |
| fields | array | `[{"label":"품명","value":"샘플"}]` | 고시 항목 정의 배열 (요청의 `fields` 가 그대로 저장됨) |
| fields_count | integer | `1` | `fields` 배열의 개수 (집계값) |
| is_active | boolean | `true` | 활성 여부 |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| icon | string | `file-alt` | 목록 UI(SortableMenuList)용 고정 아이콘 식별자 |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 (사용자 타임존 기준 문자열) |
| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 (사용자 타임존 기준 문자열) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "상품정보제공고시가 생성되었습니다.",
"data": {
"id": 3,
"name": {
"ko": "의류 고시",
"en": "Clothing Notice"
},
"localized_name": "의류 고시",
"category": "clothing",
"fields": [
{
"label": "품명",
"value": "샘플"
}
],
"fields_count": 1,
"is_active": true,
"sort_order": 0,
"icon": "file-alt",
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 10:44:49",
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 생성 처리 중 예외 발생 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.create`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
@@ -418,16 +469,67 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductNoticeTemplateResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 수정된 템플릿의 기본 키 (내부 식별자) |
| name | object | `{"ko":"의류 고시(수정)","en":"Clothing Notice (Updated)"}` | 템플릿명 (로케일별 값 객체) |
| localized_name | string | `의류 고시(수정)` | `name` 의 현재 로케일 해석 값 |
| category | string\|null | `clothing` | 이 템플릿이 적용되는 품목 카테고리 식별자 (미지정 시 `null`) |
| fields | array | `[{"label":"품명","value":"샘플"}]` | 고시 항목 정의 배열 (요청의 `fields` 로 대체됨) |
| fields_count | integer | `1` | `fields` 배열의 개수 (집계값) |
| is_active | boolean | `true` | 활성 여부 |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| icon | string | `file-alt` | 목록 UI(SortableMenuList)용 고정 아이콘 식별자 |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 (사용자 타임존 기준 문자열) |
| updated_at | string | `2026-07-08 15:00:27` | 최종 수정 일시 (사용자 타임존 기준 문자열) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "상품정보제공고시가 수정되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "의류 고시(수정)",
"en": "Clothing Notice (Updated)"
},
"localized_name": "의류 고시(수정)",
"category": "clothing",
"fields": [
{
"label": "품명",
"value": "샘플"
}
],
"fields_count": 1,
"is_active": true,
"sort_order": 0,
"icon": "file-alt",
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 15:00:27",
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 수정 처리 중 예외 발생 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
@@ -352,11 +352,129 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductResource`). 성공 시 HTTP 201._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 생성된 상품의 기본 키 (내부 식별자) |
| name | object | `{"ko":"샘플 상품","en":"Sample Product"}` | 상품명 (로케일별 값 객체) |
| name_localized | string | `샘플 상품` | `name` 의 현재 로케일 해석 값 |
| product_code | string | `PROD-GJUX-1484` | 상품코드 (상품 고유 관리 식별자) |
| sku | string | `SKU-MRAD-9306` | 재고관리코드(SKU) |
| categories | array | `[]` | 소속 카테고리 목록 (categories 관계 로드 시) |
| category_ids | array | `[1]` | 소속 카테고리 ID 배열 (categories 관계 로드 시) |
| primary_category_id | integer | `1` | 대표 카테고리 ID (is_primary 카테고리) |
| brand_id | integer | `null` | 브랜드 식별자 |
| list_price | integer | `112594` | 정가 (기본통화 자릿수로 정규화) |
| selling_price | integer | `88949` | 판매가 (기본통화 자릿수로 정규화) |
| discount_rate | integer | `21` | 할인율(%) ((1 - 판매가/정가) × 100) |
| stock_quantity | integer | `22` | 재고 수량 (옵션 사용 시 옵션 재고 합계) |
| safe_stock_quantity | integer | `12` | 안전재고 수량 |
| is_below_safe_stock | boolean | `false` | 재고가 안전재고 미만인지 여부 |
| is_stock_consistent | boolean | `true` | 상품 재고와 옵션 재고 합계의 일치 여부 |
| sales_status | string | `on_sale` | 판매상태 (on_sale / suspended / sold_out / coming_soon) |
| sales_status_label | string | `판매중` | `sales_status` 의 현지화 라벨 |
| display_status | string | `visible` | 전시상태 (visible / hidden) |
| display_status_label | string | `전시` | `display_status` 의 현지화 라벨 |
| tax_status | string | `taxable` | 과세여부 (taxable / tax_free) |
| tax_status_label | string | `과세` | `tax_status` 의 현지화 라벨 |
| tax_rate | string | `10.00` | 세율(%) |
| shipping_policy_id | integer | `null` | 배송정책 식별자 |
| common_info_id | integer | `null` | 공통정보 식별자 |
| description | object | `{"ko":"상품 설명","en":"Description"}` | 상세 설명 (로케일별 값 객체) |
| description_localized | string | `상품 설명` | `description` 의 현재 로케일 해석 값 |
| description_mode | string | `text` | 상세 설명 편집 모드 (text / html) |
| min_purchase_qty | integer | `1` | 최소 구매 수량 |
| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) |
| purchase_restriction | string | `none` | 구매 대상 제한 (none / restricted) |
| allowed_roles | array | `null` | 구매 허용 역할 ID 배열 |
| barcode | string | `null` | 바코드 |
| hs_code | string | `null` | HS 코드 (수출입 관세 분류 코드) |
| images | array | `[]` | 상품 이미지 목록 (images 관계 로드 시) |
| thumbnail_hash | string | `null` | 대표 이미지 해시 |
| thumbnail_url | string | `null` | 대표 이미지 다운로드 URL |
| meta_title | object | `null` | SEO 메타 제목 (다국어 JSON) |
| meta_description | object | `null` | SEO 메타 설명 (다국어 JSON) |
| meta_keywords | array | `null` | SEO 메타 키워드 배열 |
| seo_sync_title | boolean | `true` | SEO 제목 자동 동기화 여부 |
| seo_sync_description | boolean | `true` | SEO 설명 자동 동기화 여부 |
| has_options | boolean | `false` | 옵션 사용 여부 |
| option_groups | array | `[]` | 옵션 그룹 정의 (예: `[{"name":"색상","values":["빨강","파랑"]}]`) |
| options | array | `[]` | 생성된 옵션(SKU) 목록 (`ProductOptionResource`) |
| additional_options | array | `[]` | 추가옵션 그룹 목록 |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 상품에 수행 가능한 작업 불리언 맵 |
> `categories` / `shipping_policy` / `label_assignments` / `notice_items` 는 해당 관계가 eager load 된 경우에만 포함됩니다 (`whenLoaded`).
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "상품이 등록되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "샘플 상품",
"en": "Sample Product"
},
"name_localized": "샘플 상품",
"product_code": "PROD-GJUX-1484",
"sku": "SKU-MRAD-9306",
"list_price": 112594,
"selling_price": 88949,
"discount_rate": 21,
"stock_quantity": 22,
"safe_stock_quantity": 12,
"is_below_safe_stock": false,
"is_stock_consistent": true,
"sales_status": "on_sale",
"sales_status_label": "판매중",
"display_status": "visible",
"display_status_label": "전시",
"tax_status": "taxable",
"tax_status_label": "과세",
"tax_rate": "10.00",
"shipping_policy_id": null,
"common_info_id": null,
"description": {
"ko": "상품 설명",
"en": "Description"
},
"description_localized": "상품 설명",
"description_mode": "text",
"min_purchase_qty": 1,
"max_purchase_qty": 0,
"purchase_restriction": "none",
"allowed_roles": null,
"barcode": null,
"hs_code": null,
"thumbnail_hash": null,
"thumbnail_url": null,
"meta_title": null,
"meta_description": null,
"meta_keywords": null,
"seo_sync_title": true,
"seo_sync_description": true,
"has_options": false,
"option_groups": [],
"options": [],
"additional_options": [],
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 10:44:49",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -409,11 +527,31 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductService::bulkUpdatePrice()` 반환값)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `3` | 실제로 가격이 변경된 상품 건수 |
| requested_count | integer | `3` | 요청한 `ids` 의 개수 (대상으로 지정한 건수) |
> 응답 `message` 의 `:count` 는 `updated_count` 로 치환됩니다.
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "3개 상품의 가격이 수정되었습니다.",
"data": {
"updated_count": 3,
"requested_count": 3
}
}
```
**에러 응답**
@@ -464,11 +602,31 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductService::bulkUpdateStatus()` 반환값)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `3` | 실제로 상태가 변경된 상품 건수 |
| requested_count | integer | `3` | 요청한 `ids` 의 개수 (대상으로 지정한 건수) |
> 응답 `message` 의 `:count` 는 `updated_count` 로 치환됩니다.
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "3개 상품이 수정되었습니다.",
"data": {
"updated_count": 3,
"requested_count": 3
}
}
```
**에러 응답**
@@ -519,11 +677,31 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductService::bulkUpdateStock()` 반환값)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `3` | 실제로 재고가 변경된 상품 건수 |
| requested_count | integer | `3` | 요청한 `ids` 의 개수 (대상으로 지정한 건수) |
> 응답 `message` 의 `:count` 는 `updated_count` 로 치환됩니다.
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "3개 상품의 재고가 수정되었습니다.",
"data": {
"updated_count": 3,
"requested_count": 3
}
}
```
**에러 응답**
@@ -550,16 +728,16 @@ Content-Type: application/json
| --- | --- | --- | --- | --- | --- |
| ids | body | array | 예 | min 1 | 대상 리소스 식별자 배열 (대량 작업 대상) |
| bulk_changes | body | array | 아니오 | — | 상품 조건 기반 일괄 변경값 (지정 시 ids 전체에 sales_status/display_status 일괄 적용) |
| bulk_changes.sales_status | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| bulk_changes.display_status | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| items | body | array | 아니오 | — | 처리 대상 항목 배열 |
| bulk_changes.sales_status | body | string | 아니오 | — | `ids` 전체에 일괄 적용할 판매상태 (지정 시 items 의 개별 sales_status 보다 우선) |
| bulk_changes.display_status | body | string | 아니오 | — | `ids` 전체에 일괄 적용할 전시상태 (지정 시 items 의 개별 display_status 보다 우선) |
| items | body | array | 아니오 | — | 처리 대상 항목 배열 (각 항목: id + 개별 수정할 상품 필드 — name/list_price/selling_price/sales_status/display_status. bulk_changes 로 지정된 필드는 제외됨) |
| option_bulk_changes | body | array | 아니오 | — | 옵션 조건 기반 일괄 변경값 (price_adjustment/stock_quantity 를 method+value 로 일괄 조정) |
| option_bulk_changes.price_adjustment | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| option_bulk_changes.price_adjustment.method | body | string | 아니오 | `set`, `add`, `percent` | <!-- TODO: 용도 --> |
| option_bulk_changes.price_adjustment.value | body | number | 아니오 | — | <!-- TODO: 용도 --> |
| option_bulk_changes.stock_quantity | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| option_bulk_changes.stock_quantity.method | body | string | 아니오 | `set`, `add`, `subtract` | <!-- TODO: 용도 --> |
| option_bulk_changes.stock_quantity.value | body | integer | 아니오 | min 0 | <!-- TODO: 용도 --> |
| option_bulk_changes.price_adjustment | body | array | 아니오 | — | 대상 상품들의 모든 옵션에 적용할 옵션 추가금 일괄 변경 블록 (method + value 쌍) |
| option_bulk_changes.price_adjustment.method | body | string | 아니오 | `set`, `add`, `percent` | 옵션 추가금 변경 방식 (set 지정값으로 설정 / add 기존값에 가산 / percent 비율 적용) |
| option_bulk_changes.price_adjustment.value | body | number | 아니오 | — | 옵션 추가금 변경 값 (method 에 따라 설정값·가산값·비율로 해석) |
| option_bulk_changes.stock_quantity | body | array | 아니오 | — | 대상 상품들의 모든 옵션에 적용할 옵션 재고 일괄 변경 블록 (method + value 쌍) |
| option_bulk_changes.stock_quantity.method | body | string | 아니오 | `set`, `add`, `subtract` | 옵션 재고 변경 방식 (set 지정값으로 설정 / add 증가 / subtract 감소) |
| option_bulk_changes.stock_quantity.value | body | integer | 아니오 | min 0 | 옵션 재고 변경 수량 (method 에 따라 설정 수량·증감 수량으로 해석) |
| option_items | body | array | 아니오 | — | 옵션 개별 인라인 수정 배열 (각 항목: product_id·option_id + 수정할 옵션 필드) |
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product.bulk_update_validation_rules`).
@@ -606,11 +784,31 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductService::bulkUpdate()` 반환값)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| products_updated | integer | `3` | 반영된 상품 건수 (bulk_changes 적용 건수 + items 개별 수정 건수) |
| options_updated | integer | `5` | 반영된 옵션 건수 (`ProductOptionService::bulkUpdate()` 결과) |
> 응답 `message` 의 `:count` 는 `products_updated + options_updated` 합산값으로 치환됩니다.
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "8개 상품이 수정되었습니다.",
"data": {
"products_updated": 3,
"options_updated": 5
}
}
```
**에러 응답**
@@ -802,11 +1000,125 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductResource` — 수정 후 상품 전체). 필드 구성은 `GET /admin/products/by-code/{code}` 의 응답 필드 표와 동일합니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 수정된 상품의 기본 키 (내부 식별자) |
| name | object | `{"ko":"API 문서 샘플 상품","en":"API Doc Sample Product"}` | 상품명 (로케일별 값 객체) |
| name_localized | string | `API 문서 샘플 상품` | `name` 의 현재 로케일 해석 값 |
| product_code | string | `APIDOCSAMPLE01` | 상품코드 |
| sku | string | `SKU-DKOK-1319` | 재고관리코드(SKU) |
| categories | array | `[]` | 소속 카테고리 목록 (categories 관계 로드 시) |
| category_ids | array | `[]` | 소속 카테고리 ID 배열 |
| primary_category_id | integer | `null` | 대표 카테고리 ID |
| brand_id | integer | `null` | 브랜드 식별자 |
| list_price | integer | `703155` | 정가 (기본통화 기준) |
| selling_price | integer | `597682` | 판매가 (기본통화 기준) |
| discount_rate | integer | `15` | 할인율(%) |
| stock_quantity | integer | `509` | 재고 수량 |
| safe_stock_quantity | integer | `38` | 안전재고 수량 |
| is_below_safe_stock | boolean | `false` | 재고가 안전재고 미만인지 여부 |
| is_stock_consistent | boolean | `true` | 상품 재고와 옵션 재고 합계 일치 여부 |
| sales_status | string | `on_sale` | 판매상태 (on_sale / suspended / sold_out / coming_soon) |
| sales_status_label | string | `판매중` | `sales_status` 의 현지화 라벨 |
| display_status | string | `visible` | 전시상태 (visible / hidden) |
| display_status_label | string | `전시` | `display_status` 의 현지화 라벨 |
| tax_status | string | `taxable` | 과세여부 (taxable / tax_free) |
| tax_status_label | string | `과세` | `tax_status` 의 현지화 라벨 |
| tax_rate | string | `10.00` | 세율(%) |
| shipping_policy_id | integer | `null` | 배송정책 식별자 |
| common_info_id | integer | `null` | 공통정보 식별자 |
| description | object | `{"ko":"상품 설명","en":"Description"}` | 상세 설명 (로케일별 값 객체) |
| description_localized | string | `상품 설명` | `description` 의 현재 로케일 해석 값 |
| description_mode | string | `text` | 상세 설명 편집 모드 (text / html) |
| min_purchase_qty | integer | `1` | 최소 구매 수량 |
| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) |
| purchase_restriction | string | `none` | 구매 대상 제한 (none / restricted) |
| allowed_roles | array | `null` | 구매 허용 역할 ID 배열 |
| barcode | string | `null` | 바코드 |
| hs_code | string | `null` | HS 코드 |
| images | array | `[]` | 상품 이미지 목록 (images 관계 로드 시) |
| thumbnail_hash | string | `null` | 대표 이미지 해시 |
| thumbnail_url | string | `null` | 대표 이미지 다운로드 URL |
| meta_title | object | `null` | SEO 메타 제목 (다국어 JSON) |
| meta_description | object | `null` | SEO 메타 설명 (다국어 JSON) |
| meta_keywords | array | `null` | SEO 메타 키워드 배열 |
| seo_sync_title | boolean | `true` | SEO 제목 자동 동기화 여부 |
| seo_sync_description | boolean | `true` | SEO 설명 자동 동기화 여부 |
| has_options | boolean | `false` | 옵션 사용 여부 |
| option_groups | array | `[]` | 옵션 그룹 정의 |
| options | array | `[]` | 옵션(SKU) 목록 (`ProductOptionResource`) |
| additional_options | array | `[]` | 추가옵션 그룹 목록 |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 11:02:13` | 최종 수정 일시 (이번 수정 시각으로 갱신) |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 상품에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "상품이 수정되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "API 문서 샘플 상품",
"en": "API Doc Sample Product"
},
"name_localized": "API 문서 샘플 상품",
"product_code": "APIDOCSAMPLE01",
"sku": "SKU-DKOK-1319",
"category_ids": [],
"primary_category_id": null,
"brand_id": null,
"list_price": 703155,
"selling_price": 597682,
"discount_rate": 15,
"stock_quantity": 509,
"safe_stock_quantity": 38,
"is_below_safe_stock": false,
"is_stock_consistent": true,
"sales_status": "on_sale",
"sales_status_label": "판매중",
"display_status": "visible",
"display_status_label": "전시",
"tax_status": "taxable",
"tax_status_label": "과세",
"tax_rate": "10.00",
"shipping_policy_id": null,
"common_info_id": null,
"description_mode": "text",
"min_purchase_qty": 1,
"max_purchase_qty": 0,
"purchase_restriction": "none",
"allowed_roles": null,
"barcode": null,
"hs_code": null,
"thumbnail_hash": null,
"thumbnail_url": null,
"meta_title": null,
"meta_description": null,
"meta_keywords": null,
"seo_sync_title": true,
"seo_sync_description": true,
"has_options": false,
"option_groups": [],
"options": [],
"additional_options": [],
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 11:02:13",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -843,11 +1155,27 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| product_code | string | `PROD-GJUX-1484` | `ProductService::generateUniqueCode()` 가 발급한, 기존 상품과 중복되지 않는 신규 상품코드 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "상품코드가 생성되었습니다.",
"data": {
"product_code": "PROD-GJUX-1484"
}
}
```
**에러 응답**
@@ -875,8 +1203,8 @@ Authorization: Bearer {YOUR_TOKEN}
| temp_key | body | string | 아니오 | max 64 | 임시 업로드 세션 키 (같은 상품의 여러 이미지를 한 세션으로 묶음, 생략 시 서버가 UUID 자동 발급) |
| collection | body | string | 아니오 | — | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) |
| alt_text | body | array | 아니오 | — | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) |
| alt_text.ko | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| alt_text.en | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| alt_text.ko | body | string | 아니오 | max 255 | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) — `ko` 로케일 값 |
| alt_text.en | body | string | 아니오 | max 255 | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) — `en` 로케일 값 |
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-image.filter_upload_validation_rules`).
@@ -919,19 +1247,57 @@ Content-Disposition: form-data; name="alt_text.en"
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드. FileUploader 컴포넌트 규약에 맞춰 실제 이미지 정보는 `data.data` 로 한 단계 더 감싸져 있습니다. 성공 시 HTTP 201._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| data | object | `{ ... }` | 업로드된 이미지 정보 객체 (아래 하위 필드) |
| data.id | integer | `1` | 업로드된 이미지 레코드의 기본 키 |
| data.hash | string | `3f7a1c9e…` | 이미지 해시 (상품 생성/수정 시 `thumbnail_hash` 지정에 사용) |
| data.original_filename | string | `product-main.jpg` | 업로드한 원본 파일명 |
| data.mime_type | string | `image/jpeg` | 파일 MIME 타입 |
| data.size | integer | `204800` | 파일 크기 (바이트) |
| data.size_formatted | string | `200 KB` | 사람이 읽는 파일 크기 문자열 (B/KB/MB/GB) |
| data.download_url | string | `/api/modules/sirsoft-ecommerce/products/images/3f7a1c9e/download` | 이미지 다운로드/표시 URL |
| data.order | integer | `1` | 이미지 노출 순서 (`sort_order`, 미지정 시 1) |
| data.is_image | boolean | `true` | MIME 타입이 `image/` 로 시작하는지 여부 |
| data.is_thumbnail | boolean | `true` | 대표 이미지 여부 (컬렉션의 첫 이미지는 자동 지정) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "이미지가 업로드되었습니다.",
"data": {
"data": {
"id": 1,
"hash": "3f7a1c9e",
"original_filename": "product-main.jpg",
"mime_type": "image/jpeg",
"size": 204800,
"size_formatted": "200 KB",
"download_url": "/api/modules/sirsoft-ecommerce/products/images/3f7a1c9e/download",
"order": 1,
"is_image": true,
"is_thumbnail": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 파일 저장 등 업로드 처리 중 오류가 발생한 경우 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) 또는 상품 이미지 개수 상한 초과 (`ProductImageUploadLimitException`) |
<!-- @generated:end -->
@@ -970,16 +1336,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 데이터를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "이미지 순서가 변경되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 순서 갱신 처리 중 오류가 발생한 경우 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
@@ -1012,19 +1389,30 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 데이터를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "이미지가 삭제되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 삭제 처리 중 오류가 발생한 경우 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | `id` 에 해당하는 이미지가 없는 경우 (`messages.product_images.not_found`) |
<!-- @generated:end -->
@@ -1134,8 +1522,8 @@ Authorization: Bearer {YOUR_TOKEN}
| temp_key | body | string | 아니오 | max 64 | 임시 업로드 세션 키 (같은 상품의 여러 이미지를 한 세션으로 묶음, 생략 시 서버가 UUID 자동 발급) |
| collection | body | string | 아니오 | — | 첨부 컬렉션 그룹명 (첨부를 용도별로 묶는 키, 미지정 시 default) |
| alt_text | body | array | 아니오 | — | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) |
| alt_text.ko | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| alt_text.en | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| alt_text.ko | body | string | 아니오 | max 255 | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) — `ko` 로케일 값 |
| alt_text.en | body | string | 아니오 | max 255 | 이미지 대체 텍스트 (접근성/이미지 미표시 시 대체 문구) — `en` 로케일 값 |
> 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-ecommerce.product-image.filter_upload_validation_rules`).
@@ -1178,20 +1566,58 @@ Content-Disposition: form-data; name="alt_text.en"
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드. FileUploader 컴포넌트 규약에 맞춰 실제 이미지 정보는 `data.data` 로 한 단계 더 감싸져 있습니다. 성공 시 HTTP 201._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| data | object | `{ ... }` | 업로드된 이미지 정보 객체 (아래 하위 필드) |
| data.id | integer | `1` | 업로드된 이미지 레코드의 기본 키 |
| data.hash | string | `3f7a1c9e…` | 이미지 해시 (상품 수정 시 `thumbnail_hash` 지정에 사용) |
| data.original_filename | string | `product-main.jpg` | 업로드한 원본 파일명 |
| data.mime_type | string | `image/jpeg` | 파일 MIME 타입 |
| data.size | integer | `204800` | 파일 크기 (바이트) |
| data.size_formatted | string | `200 KB` | 사람이 읽는 파일 크기 문자열 (B/KB/MB/GB) |
| data.download_url | string | `/api/modules/sirsoft-ecommerce/products/images/3f7a1c9e/download` | 이미지 다운로드/표시 URL |
| data.order | integer | `1` | 이미지 노출 순서 (`sort_order`) |
| data.is_image | boolean | `true` | MIME 타입이 `image/` 로 시작하는지 여부 |
| data.is_thumbnail | boolean | `true` | 대표 이미지 여부 (컬렉션의 첫 이미지는 자동 지정) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "이미지가 업로드되었습니다.",
"data": {
"data": {
"id": 1,
"hash": "3f7a1c9e",
"original_filename": "product-main.jpg",
"mime_type": "image/jpeg",
"size": 204800,
"size_formatted": "200 KB",
"download_url": "/api/modules/sirsoft-ecommerce/products/images/3f7a1c9e/download",
"order": 1,
"is_image": true,
"is_thumbnail": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 파일 저장 등 업로드 처리 중 오류가 발생한 경우 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) 또는 상품 이미지 개수 상한 초과 (`ProductImageUploadLimitException`) |
<!-- @generated:end -->
@@ -1222,16 +1648,27 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 데이터를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "대표 이미지가 설정되었습니다.",
"data": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 대표 이미지 설정 처리 중 오류가 발생한 경우 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
@@ -1418,11 +1855,125 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ProductResource` — 수정 후 상품 전체). 필드 구성은 `GET /admin/products/by-code/{code}` 의 응답 필드 표와 동일합니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 수정된 상품의 기본 키 (내부 식별자) |
| name | object | `{"ko":"API 문서 샘플 상품","en":"API Doc Sample Product"}` | 상품명 (로케일별 값 객체) |
| name_localized | string | `API 문서 샘플 상품` | `name` 의 현재 로케일 해석 값 |
| product_code | string | `APIDOCSAMPLE01` | 상품코드 |
| sku | string | `SKU-DKOK-1319` | 재고관리코드(SKU) |
| categories | array | `[]` | 소속 카테고리 목록 (categories 관계 로드 시) |
| category_ids | array | `[]` | 소속 카테고리 ID 배열 |
| primary_category_id | integer | `null` | 대표 카테고리 ID |
| brand_id | integer | `null` | 브랜드 식별자 |
| list_price | integer | `703155` | 정가 (기본통화 기준) |
| selling_price | integer | `597682` | 판매가 (기본통화 기준) |
| discount_rate | integer | `15` | 할인율(%) |
| stock_quantity | integer | `509` | 재고 수량 |
| safe_stock_quantity | integer | `38` | 안전재고 수량 |
| is_below_safe_stock | boolean | `false` | 재고가 안전재고 미만인지 여부 |
| is_stock_consistent | boolean | `true` | 상품 재고와 옵션 재고 합계 일치 여부 |
| sales_status | string | `on_sale` | 판매상태 (on_sale / suspended / sold_out / coming_soon) |
| sales_status_label | string | `판매중` | `sales_status` 의 현지화 라벨 |
| display_status | string | `visible` | 전시상태 (visible / hidden) |
| display_status_label | string | `전시` | `display_status` 의 현지화 라벨 |
| tax_status | string | `taxable` | 과세여부 (taxable / tax_free) |
| tax_status_label | string | `과세` | `tax_status` 의 현지화 라벨 |
| tax_rate | string | `10.00` | 세율(%) |
| shipping_policy_id | integer | `null` | 배송정책 식별자 |
| common_info_id | integer | `null` | 공통정보 식별자 |
| description | object | `{"ko":"상품 설명","en":"Description"}` | 상세 설명 (로케일별 값 객체) |
| description_localized | string | `상품 설명` | `description` 의 현재 로케일 해석 값 |
| description_mode | string | `text` | 상세 설명 편집 모드 (text / html) |
| min_purchase_qty | integer | `1` | 최소 구매 수량 |
| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) |
| purchase_restriction | string | `none` | 구매 대상 제한 (none / restricted) |
| allowed_roles | array | `null` | 구매 허용 역할 ID 배열 |
| barcode | string | `null` | 바코드 |
| hs_code | string | `null` | HS 코드 |
| images | array | `[]` | 상품 이미지 목록 (images 관계 로드 시) |
| thumbnail_hash | string | `null` | 대표 이미지 해시 |
| thumbnail_url | string | `null` | 대표 이미지 다운로드 URL |
| meta_title | object | `null` | SEO 메타 제목 (다국어 JSON) |
| meta_description | object | `null` | SEO 메타 설명 (다국어 JSON) |
| meta_keywords | array | `null` | SEO 메타 키워드 배열 |
| seo_sync_title | boolean | `true` | SEO 제목 자동 동기화 여부 |
| seo_sync_description | boolean | `true` | SEO 설명 자동 동기화 여부 |
| has_options | boolean | `false` | 옵션 사용 여부 |
| option_groups | array | `[]` | 옵션 그룹 정의 |
| options | array | `[]` | 옵션(SKU) 목록 (`ProductOptionResource`) |
| additional_options | array | `[]` | 추가옵션 그룹 목록 |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 11:02:13` | 최종 수정 일시 (이번 수정 시각으로 갱신) |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 상품에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "상품이 수정되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "API 문서 샘플 상품",
"en": "API Doc Sample Product"
},
"name_localized": "API 문서 샘플 상품",
"product_code": "APIDOCSAMPLE01",
"sku": "SKU-DKOK-1319",
"category_ids": [],
"primary_category_id": null,
"brand_id": null,
"list_price": 703155,
"selling_price": 597682,
"discount_rate": 15,
"stock_quantity": 509,
"safe_stock_quantity": 38,
"is_below_safe_stock": false,
"is_stock_consistent": true,
"sales_status": "on_sale",
"sales_status_label": "판매중",
"display_status": "visible",
"display_status_label": "전시",
"tax_status": "taxable",
"tax_status_label": "과세",
"tax_rate": "10.00",
"shipping_policy_id": null,
"common_info_id": null,
"description_mode": "text",
"min_purchase_qty": 1,
"max_purchase_qty": 0,
"purchase_restriction": "none",
"allowed_roles": null,
"barcode": null,
"hs_code": null,
"thumbnail_hash": null,
"thumbnail_url": null,
"meta_title": null,
"meta_description": null,
"meta_keywords": null,
"seo_sync_title": true,
"seo_sync_description": true,
"has_options": false,
"option_groups": [],
"options": [],
"additional_options": [],
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 11:02:13",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -2386,9 +2937,51 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
_목록 응답: `data[]` 배열 항목의 필드 (`ProductListResource` — `GET /products/new` 와 동일 구성). `ids` 가 비어 있으면 `data` 는 빈 배열입니다._
<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. -->
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `322` | 기본 키 (내부 식별자) |
| name | object | `{"ko":"eum et quia","en":"tenetur id quae"}` | 상품명 (로케일별 값 객체) |
| name_localized | string | `eum et quia` | `name` 의 현재 로케일 해석 값 |
| product_code | string | `PROD-GJUX-1484` | 상품코드 (상품 고유 관리 식별자) |
| sku | string | `SKU-MRAD-9306` | 재고관리코드(SKU) |
| thumbnail_url | string | `/api/modules/sirsoft-ecommerce/products/images/3f7a1c9e/download` | 대표 이미지 URL |
| list_price | integer | `112594` | 정가 (기본통화 자릿수로 정규화) |
| list_price_formatted | string | `112,594원` | `list_price` 의 표시용 통화 포맷 문자열 |
| selling_price | integer | `88949` | 판매가 (기본통화 자릿수로 정규화) |
| selling_price_formatted | string | `88,949원` | `selling_price` 의 표시용 통화 포맷 문자열 |
| discount_rate | integer | `21` | 할인율(%) ((1 - 판매가/정가) × 100) |
| multi_currency_list_price | object | `{"KRW":{"price":112594,"formatted":"112,594원","is_default":true,"editable":true}}` | 통화별 정가 맵 |
| multi_currency_selling_price | object | `{"KRW":{"price":88949,"formatted":"88,949원","is_default":true,"editable":true}}` | 통화별 판매가 맵 |
| stock_quantity | integer | `22` | 재고 수량 |
| safe_stock_quantity | integer | `12` | 안전재고 수량 |
| is_below_safe_stock | boolean | `false` | 재고가 안전재고 미만인지 여부 |
| option_stock_sum | integer | `51` | 활성 옵션의 재고 합계 (options 관계 로드 시) |
| sales_status | string | `on_sale` | 판매상태 (on_sale / suspended / sold_out / coming_soon) |
| sales_status_label | string | `판매중` | `sales_status` 의 현지화 라벨 |
| sales_status_variant | string | `success` | `sales_status` 의 UI 배지 변형 키 |
| display_status | string | `visible` | 전시상태 (visible / hidden) |
| display_status_label | string | `전시` | `display_status` 의 현지화 라벨 |
| display_status_variant | string | `success` | `display_status` 의 UI 배지 변형 키 |
| categories | array | `[]` | 소속 카테고리 목록 (categories 관계 로드 시) |
| primary_category | string | `스마트폰` | 대표 카테고리명 (categories 관계 로드 시) |
| categories_with_path | array | `[]` | 소속 카테고리 + 경로(breadcrumb) 목록 |
| brand_name | string | `ASUS` | 브랜드명 (brand 관계 로드 시) |
| shipping_policy_id | integer | `31` | 배송정책 식별자 |
| shipping_policy_name | string | `국내 무료배송` | 배송정책명 (shippingPolicy 관계 로드 시) |
| min_purchase_qty | integer | `1` | 최소 구매 수량 |
| max_purchase_qty | integer | `0` | 최대 구매 수량 (0=무제한) |
| has_options | boolean | `false` | 옵션 사용 여부 |
| options_count | integer | `1` | 활성 옵션 개수 (options 관계 로드 시) |
| options | array | `[]` | 활성 옵션(SKU) 목록 (`ProductOptionResource`) |
| labels | array | `[]` | 노출 중인 상품 라벨 목록 (activeLabelAssignments 관계 로드 시) |
| review_count | integer | `0` | 리뷰 개수 |
| rating_avg | number | `0` | 평균 별점 (소수 1자리 반올림) |
| created_at | string | `2026-07-07 14:47:31` | 생성 일시 |
| updated_at | string | `2026-07-07 14:47:31` | 최종 수정 일시 |
| is_owner | boolean | `false` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 |
| abilities | object | `{"can_update":false,"can_delete":false}` | 현재 사용자가 이 상품에 수행 가능한 작업 불리언 맵 |
**응답 예시**
@@ -2854,11 +3447,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드. 성공 시 HTTP 201._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 생성된 상품 문의 글의 기본 키 (게시판 모듈에 생성된 게시글 ID) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "문의가 등록되었습니다.",
"data": {
"id": 1
}
}
```
**에러 응답**
@@ -2866,7 +3475,7 @@ Content-Type: application/json
| --- | --- | --- |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) 또는 도메인 규칙 위반(비밀글 비허용 등, `RuntimeException` → `messages.inquiries.create_failed`) |
<!-- @generated:end -->
@@ -267,11 +267,150 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (생성된 쿠폰 = `CouponResource`, 상세 조회(show)와 동일한 필드 집합)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 생성된 쿠폰의 기본 키 |
| name | object | `{"ko":"신규가입 쿠폰","en":"Signup Coupon"}` | 쿠폰명 (로케일별 값 객체) |
| localized_name | string | `신규가입 쿠폰` | `name` 의 현재 로케일 해석 값 |
| description | object\|null | `null` | 쿠폰 설명 (로케일별 값 객체) |
| localized_description | string\|null | `null` | `description` 의 현재 로케일 해석 값 |
| target_type | string | `order_amount` | 적용대상: product_amount(상품금액), order_amount(주문금액), shipping_fee(배송비) |
| target_type_label | string | `주문금액` | `target_type` 의 다국어 라벨 |
| target_type_badge_color | string | `blue` | `target_type` 배지 색상 (상품금액=teal, 주문금액=blue, 배송비=orange) |
| discount_type | string | `fixed` | 혜택유형: fixed(정액), rate(정률 %) |
| discount_type_label | string | `정액할인` | `discount_type` 의 다국어 라벨 |
| discount_value | number | `1000` | 혜택값 (정액=할인 금액, 정률=할인율 %) |
| discount_max_amount | number\|null | `null` | 최대 할인액 (정률 시 상한, 미설정 null) |
| min_order_amount | number | `0` | 쿠폰 적용 최소 주문금액 (0=제한 없음) |
| benefit_formatted | string | `1,000원 할인` | 혜택 표시용 포맷 문자열 |
| multi_currency_discount_value | object\|null | `{"KRW":{"price":1000,"formatted":"1,000원","is_default":true,"editable":true}}` | 정액 할인 금액의 통화별 환산 맵 (정률은 null) |
| multi_currency_min_order_amount | object\|null | `null` | 최소 주문금액의 통화별 환산 맵 (0이면 null) |
| multi_currency_discount_max_amount | object\|null | `null` | 최대 할인액의 통화별 환산 맵 (미설정 시 null) |
| issue_method | string | `download` | 발급방법: direct(직접발급), download(다운로드), auto(자동발급) |
| issue_method_label | string | `다운로드` | `issue_method` 의 다국어 라벨 |
| issue_method_badge_color | string | `teal` | `issue_method` 배지 색상 (직접발급=gray, 다운로드=teal, 자동발급=blue) |
| issue_condition | string | `manual` | 발급조건: manual(수동), signup(회원가입), first_purchase(첫구매), birthday(생일) |
| issue_condition_label | string | `수동발급` | `issue_condition` 의 다국어 라벨 |
| issue_condition_badge_color | string | `orange` | `issue_condition` 배지 색상 |
| issue_status | string | `issuing` | 발급상태: issuing(발급중), stopped(발급중단) |
| issue_status_label | string | `발급중` | `issue_status` 의 다국어 라벨 |
| issue_status_badge_color | string | `blue` | `issue_status` 배지 색상 (발급중=blue, 발급중단=orange) |
| total_quantity | integer\|null | `null` | 총 발급 수량 (null=무제한) |
| issued_count | integer | `0` | 현재까지 발급된 수량 (생성 직후 0) |
| per_user_limit | integer | `1` | 회원 1인당 발급 제한 수량 (0=무제한) |
| issue_count_formatted | string | `0/무제한` | 발급 수량 표시용 포맷 문자열 |
| valid_type | string | `period` | 유효기간 유형: period(기간 지정), days_from_issue(발급일로부터 N일) |
| valid_days | integer\|null | `null` | 발급일로부터 유효 일수 (valid_type=days_from_issue) |
| valid_from | string\|null | `2026-07-01` | 유효기간 시작일 |
| valid_to | string\|null | `2026-07-31` | 유효기간 종료일 |
| valid_period_formatted | string | `-` | 유효기간 표시용 포맷 문자열 |
| issue_from | string\|null | `null` | 발급기간 시작 일시 (datetime-local 호환) |
| issue_to | string\|null | `null` | 발급기간 종료 일시 (datetime-local 호환) |
| issue_period_formatted | string | `상시발급` | 발급기간 표시용 포맷 문자열 |
| is_combinable | boolean | `false` | 다른 쿠폰과 중복 사용 가능 여부 |
| target_scope | string | `all` | 적용 범위: all(전체 상품), products(특정 상품), categories(특정 카테고리) |
| target_scope_label | string | `전체상품` | `target_scope` 의 다국어 라벨 |
| is_issuable | boolean | `true` | 현재 발급 가능 상태인지 여부 |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 |
| created_by | string\|null | `a1e0a91a-fba6-491c-a53e-7285a5686857` | 등록자(생성한 관리자) 식별자 |
| created_by_name | string | `관리자` | 등록자 이름 (미설정 시 `-`) |
| created_by_email | string\|null | `admin@example.com` | 등록자 이메일 (creator 관계 파생) |
| creator | object\|null | `{"uuid":"a1e0a91a-…","name":"관리자"}` | 생성자 정보 객체 |
| included_products | array | `[]` | 적용 대상 포함 상품 목록 (target_scope=products) |
| excluded_products | array | `[]` | 적용 제외 상품 목록 |
| included_categories | array | `[]` | 적용 대상 포함 카테고리 목록 (target_scope=categories) |
| excluded_categories | array | `[]` | 적용 제외 카테고리 목록 |
| issues_count | integer | `0` | 발급 내역 건수 (집계) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "쿠폰이 등록되었습니다.",
"data": {
"id": 2,
"name": {
"ko": "신규가입 쿠폰",
"en": "Signup Coupon"
},
"localized_name": "신규가입 쿠폰",
"description": null,
"localized_description": null,
"target_type": "order_amount",
"target_type_label": "주문금액",
"target_type_badge_color": "blue",
"discount_type": "fixed",
"discount_type_label": "정액할인",
"discount_value": 1000,
"discount_max_amount": null,
"min_order_amount": 0,
"benefit_formatted": "1,000원 할인",
"multi_currency_discount_value": {
"KRW": {
"price": 1000,
"formatted": "1,000원",
"is_default": true,
"editable": true
}
},
"multi_currency_min_order_amount": null,
"multi_currency_discount_max_amount": null,
"issue_method": "download",
"issue_method_label": "다운로드",
"issue_method_badge_color": "teal",
"issue_condition": "manual",
"issue_condition_label": "수동발급",
"issue_condition_badge_color": "orange",
"issue_status": "issuing",
"issue_status_label": "발급중",
"issue_status_badge_color": "blue",
"total_quantity": null,
"issued_count": 0,
"per_user_limit": 1,
"issue_count_formatted": "0/무제한",
"valid_type": "period",
"valid_days": null,
"valid_from": "2026-07-01",
"valid_to": "2026-07-31",
"valid_period_formatted": "2026-07-01 ~ 2026-07-31",
"issue_from": null,
"issue_to": null,
"issue_period_formatted": "상시발급",
"is_combinable": false,
"target_scope": "all",
"target_scope_label": "전체상품",
"is_issuable": true,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 10:44:49",
"created_by": "a1e0a91a-fba6-491c-a53e-7285a5686857",
"created_by_name": "관리자",
"created_by_email": "admin@example.com",
"creator": {
"uuid": "a1e0a91a-fba6-491c-a53e-7285a5686857",
"name": "관리자"
},
"included_products": [],
"excluded_products": [],
"included_categories": [],
"excluded_categories": [],
"issues_count": 0,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -318,11 +457,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `3` | 발급상태가 실제로 변경된 쿠폰 건수 (`CouponService::bulkUpdateIssueStatus()` 반환값) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "3개 쿠폰의 발급상태가 변경되었습니다.",
"data": {
"updated_count": 3
}
}
```
**에러 응답**
@@ -502,16 +657,156 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (수정된 쿠폰 = `CouponResource`, 상세 조회(show)와 동일한 필드 집합)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 수정된 쿠폰의 기본 키 |
| name | object | `{"ko":"API 문서 샘플 쿠폰(수정)","en":"API Doc Sample Coupon (edited)"}` | 쿠폰명 (로케일별 값 객체) |
| localized_name | string | `API 문서 샘플 쿠폰(수정)` | `name` 의 현재 로케일 해석 값 |
| description | object\|null | `null` | 쿠폰 설명 (로케일별 값 객체) |
| localized_description | string\|null | `null` | `description` 의 현재 로케일 해석 값 |
| target_type | string | `order_amount` | 적용대상: product_amount, order_amount, shipping_fee |
| target_type_label | string | `주문금액` | `target_type` 의 다국어 라벨 |
| target_type_badge_color | string | `blue` | `target_type` 배지 색상 |
| discount_type | string | `fixed` | 혜택유형: fixed(정액), rate(정률 %) |
| discount_type_label | string | `정액할인` | `discount_type` 의 다국어 라벨 |
| discount_value | number | `2000` | 혜택값 (정액=할인 금액, 정률=할인율 %) |
| discount_max_amount | number\|null | `null` | 최대 할인액 (정률 시 상한) |
| min_order_amount | number | `0` | 쿠폰 적용 최소 주문금액 (0=제한 없음) |
| benefit_formatted | string | `2,000원 할인` | 혜택 표시용 포맷 문자열 |
| multi_currency_discount_value | object\|null | `{"KRW":{"price":2000,"formatted":"2,000원","is_default":true,"editable":true}}` | 정액 할인 금액의 통화별 환산 맵 (정률은 null) |
| multi_currency_min_order_amount | object\|null | `null` | 최소 주문금액의 통화별 환산 맵 (0이면 null) |
| multi_currency_discount_max_amount | object\|null | `null` | 최대 할인액의 통화별 환산 맵 (미설정 시 null) |
| issue_method | string | `download` | 발급방법: direct, download, auto |
| issue_method_label | string | `다운로드` | `issue_method` 의 다국어 라벨 |
| issue_method_badge_color | string | `teal` | `issue_method` 배지 색상 |
| issue_condition | string | `manual` | 발급조건: manual, signup, first_purchase, birthday |
| issue_condition_label | string | `수동발급` | `issue_condition` 의 다국어 라벨 |
| issue_condition_badge_color | string | `orange` | `issue_condition` 배지 색상 |
| issue_status | string | `issuing` | 발급상태: issuing(발급중), stopped(발급중단) |
| issue_status_label | string | `발급중` | `issue_status` 의 다국어 라벨 |
| issue_status_badge_color | string | `blue` | `issue_status` 배지 색상 |
| total_quantity | integer\|null | `null` | 총 발급 수량 (null=무제한) |
| issued_count | integer | `0` | 현재까지 발급된 수량 |
| per_user_limit | integer | `1` | 회원 1인당 발급 제한 수량 (0=무제한) |
| issue_count_formatted | string | `0/무제한` | 발급 수량 표시용 포맷 문자열 |
| valid_type | string | `period` | 유효기간 유형: period, days_from_issue |
| valid_days | integer\|null | `null` | 발급일로부터 유효 일수 |
| valid_from | string\|null | `2026-07-01` | 유효기간 시작일 |
| valid_to | string\|null | `2026-07-31` | 유효기간 종료일 |
| valid_period_formatted | string | `2026-07-01 ~ 2026-07-31` | 유효기간 표시용 포맷 문자열 |
| issue_from | string\|null | `null` | 발급기간 시작 일시 |
| issue_to | string\|null | `null` | 발급기간 종료 일시 |
| issue_period_formatted | string | `상시발급` | 발급기간 표시용 포맷 문자열 |
| is_combinable | boolean | `false` | 다른 쿠폰과 중복 사용 가능 여부 |
| target_scope | string | `all` | 적용 범위: all, products, categories |
| target_scope_label | string | `전체상품` | `target_scope` 의 다국어 라벨 |
| is_issuable | boolean | `true` | 현재 발급 가능 상태인지 여부 |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 11:02:13` | 최종 수정 일시 (수정 반영) |
| created_by | string\|null | `a1e0a91a-fba6-491c-a53e-7285a5686857` | 등록자 식별자 |
| created_by_name | string | `관리자` | 등록자 이름 (미설정 시 `-`) |
| created_by_email | string\|null | `admin@example.com` | 등록자 이메일 |
| creator | object\|null | `{"uuid":"a1e0a91a-…","name":"관리자"}` | 생성자 정보 객체 |
| included_products | array | `[]` | 적용 대상 포함 상품 목록 (수정 시 동기화 결과) |
| excluded_products | array | `[]` | 적용 제외 상품 목록 |
| included_categories | array | `[]` | 적용 대상 포함 카테고리 목록 |
| excluded_categories | array | `[]` | 적용 제외 카테고리 목록 |
| issues_count | integer | `0` | 발급 내역 건수 (집계) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "쿠폰이 수정되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "API 문서 샘플 쿠폰(수정)",
"en": "API Doc Sample Coupon (edited)"
},
"localized_name": "API 문서 샘플 쿠폰(수정)",
"description": null,
"localized_description": null,
"target_type": "order_amount",
"target_type_label": "주문금액",
"target_type_badge_color": "blue",
"discount_type": "fixed",
"discount_type_label": "정액할인",
"discount_value": 2000,
"discount_max_amount": null,
"min_order_amount": 0,
"benefit_formatted": "2,000원 할인",
"multi_currency_discount_value": {
"KRW": {
"price": 2000,
"formatted": "2,000원",
"is_default": true,
"editable": true
}
},
"multi_currency_min_order_amount": null,
"multi_currency_discount_max_amount": null,
"issue_method": "download",
"issue_method_label": "다운로드",
"issue_method_badge_color": "teal",
"issue_condition": "manual",
"issue_condition_label": "수동발급",
"issue_condition_badge_color": "orange",
"issue_status": "issuing",
"issue_status_label": "발급중",
"issue_status_badge_color": "blue",
"total_quantity": null,
"issued_count": 0,
"per_user_limit": 1,
"issue_count_formatted": "0/무제한",
"valid_type": "period",
"valid_days": null,
"valid_from": "2026-07-01",
"valid_to": "2026-07-31",
"valid_period_formatted": "2026-07-01 ~ 2026-07-31",
"issue_from": null,
"issue_to": null,
"issue_period_formatted": "상시발급",
"is_combinable": false,
"target_scope": "all",
"target_scope_label": "전체상품",
"is_issuable": true,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 11:02:13",
"created_by": "a1e0a91a-fba6-491c-a53e-7285a5686857",
"created_by_name": "관리자",
"created_by_email": "admin@example.com",
"creator": {
"uuid": "a1e0a91a-fba6-491c-a53e-7285a5686857",
"name": "관리자"
},
"included_products": [],
"excluded_products": [],
"included_categories": [],
"excluded_categories": [],
"issues_count": 0,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 수정 처리 중 예외 발생 시 (`exceptions.operation_failed`: "작업 처리 중 오류가 발생했습니다.") — 대상 쿠폰 미존재도 이 경로로 응답 |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
@@ -553,16 +848,44 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`CouponService::issueDirectly()` 반환 구조)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| issued | integer | `2` | 실제로 발급에 성공한 회원 수 |
| skipped | array | `[{"user_id":15,"reason":"1인당 발급 가능 수량을 초과했습니다."}]` | 발급이 건너뛰어진 회원 목록 (전체 롤백 없이 회원 단위로 누적) |
| skipped[].user_id | integer | `15` | 건너뛴 회원의 내부 회원 ID |
| skipped[].reason | string | `1인당 발급 가능 수량을 초과했습니다.` | 건너뛴 사유 (발급 검증 예외 메시지 원문) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "2명에게 발급했습니다. (1명은 발급 조건 미충족으로 제외)",
"data": {
"issued": 2,
"skipped": [
{
"user_id": 15,
"reason": "1인당 발급 가능 수량을 초과했습니다."
}
]
}
}
```
> 건너뛴 회원이 없으면 메시지는 `messages.coupons.direct_issued`(`":issued명에게 쿠폰을 발급했습니다."`)이고 `skipped` 는 `[]` 입니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 쿠폰 미존재 또는 쿠폰 자체가 발급 불가 상태(발급중단/기간 외/수량 소진 등)여서 전원 차단된 경우 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
@@ -601,7 +924,30 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_목록 응답: `data.data[]` 배열 항목의 필드 (`CouponIssueResource`) + `data.pagination`._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 발급 내역 ID (기본 키) |
| coupon_id | integer | `1` | 발급된 쿠폰(정의) ID |
| user_id | string\|null | `a1e0a91a-fba6-491c-a53e-7285a5686857` | 발급 대상 회원의 UUID (내부 정수 ID 대신 UUID 노출) |
| coupon_code | string\|null | `WELCOME2026` | 쿠폰 코드 (다운로드 쿠폰 시 발급되는 코드, 없으면 null) |
| status | string | `available` | 발급 내역 상태: available(사용가능), used(사용완료), expired(만료), cancelled(취소) |
| status_label | string | `사용가능` | `status` 의 다국어 라벨 (사용가능/사용완료/기간만료/취소됨) |
| status_badge_color | string | `blue` | `status` 배지 색상 (available=blue, used=green, expired=gray, cancelled=red) |
| issued_at | string\|null | `2026-07-08 10:50:11` | 발급 일시 (사용자 타임존 포맷) |
| expired_at | string\|null | `2026-07-31 23:59:59` | 만료 일시 (사용자 타임존 포맷, 없으면 null) |
| used_at | string\|null | `null` | 사용 일시 (미사용 시 null) |
| order_id | integer\|null | `null` | 쿠폰이 사용된 주문 ID (미사용 시 null) |
| order_number | string\|null | `20260708-0001` | 사용된 주문의 주문번호 (order 관계 로드 시) |
| discount_amount | number\|null | `null` | 실제 적용된 할인 금액 (기본 통화 자릿수로 반올림, 미사용 시 null) |
| is_used | boolean | `false` | 사용 완료 여부 (`status === 'used'`) |
| is_expired | boolean | `false` | 만료 여부 |
| is_usable | boolean | `true` | 현재 사용 가능 여부 |
| is_cancellable | boolean | `true` | 관리자 취소 가능 여부 (available 이면서 미만료일 때만 true) |
| user_name | string | `홍길동` | 발급 대상 회원 이름 (user 관계 로드 시) |
`data.pagination` 은 `current_page`, `last_page`, `per_page`, `total`, `from`, `to`, `has_more_pages` 를 포함합니다.
**응답 예시**
@@ -645,16 +991,65 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (취소 처리된 발급 내역 = `CouponIssueResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 취소된 발급 내역 ID |
| coupon_id | integer | `1` | 발급된 쿠폰(정의) ID |
| user_id | string\|null | `a1e0a91a-fba6-491c-a53e-7285a5686857` | 발급 대상 회원의 UUID |
| coupon_code | string\|null | `WELCOME2026` | 쿠폰 코드 (다운로드 쿠폰 시) |
| status | string | `cancelled` | 취소 처리 후 상태 (항상 `cancelled`) |
| status_label | string | `취소됨` | `status` 의 다국어 라벨 |
| status_badge_color | string | `red` | `status` 배지 색상 (cancelled=red) |
| issued_at | string\|null | `2026-07-08 10:50:11` | 발급 일시 |
| expired_at | string\|null | `2026-07-31 23:59:59` | 만료 일시 |
| used_at | string\|null | `null` | 사용 일시 (미사용 건만 취소 가능하므로 항상 null) |
| order_id | integer\|null | `null` | 사용된 주문 ID (미사용이므로 null) |
| discount_amount | number\|null | `null` | 실제 적용된 할인 금액 (미사용이므로 null) |
| is_used | boolean | `false` | 사용 완료 여부 |
| is_expired | boolean | `false` | 만료 여부 |
| is_usable | boolean | `false` | 현재 사용 가능 여부 (취소되었으므로 false) |
| is_cancellable | boolean | `false` | 추가 취소 가능 여부 (이미 취소되어 false) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "쿠폰 발급이 취소되었습니다.",
"data": {
"id": 1,
"coupon_id": 1,
"user_id": "a1e0a91a-fba6-491c-a53e-7285a5686857",
"coupon_code": "WELCOME2026",
"status": "cancelled",
"status_label": "취소됨",
"status_badge_color": "red",
"issued_at": "2026-07-08 10:50:11",
"expired_at": "2026-07-31 23:59:59",
"used_at": null,
"order_id": null,
"discount_amount": null,
"is_used": false,
"is_expired": false,
"is_usable": false,
"is_cancellable": false
}
}
```
> `order_number` / `user_name` 은 관계가 로드된 경우에만 포함됩니다 (취소 응답에서는 관계를 별도 로드하지 않으므로 생략될 수 있음).
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 발급 내역 미존재(쿠폰-발급 조합 불일치 포함) 또는 미사용(available) 상태가 아니어서 취소 불가한 경우. `error.detail` 에 사유 원문("쿠폰 발급 내역을 찾을 수 없습니다." / "미사용 상태의 발급 건만 취소할 수 있습니다.")이 담김 |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
@@ -39,17 +39,41 @@ Accept: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 JSON 봉투(`data`)를 반환하지 않습니다. 성공 시 이미지 바이너리 스트림(`StreamedResponse`)을 그대로 반환합니다 (`ProductReviewImageService::download()` → `StorageInterface::response()`)._
성공 응답의 헤더는 다음과 같습니다.
| 헤더 | 값 | 용도/설명 |
| --- | --- | --- |
| Content-Type | `image/jpeg` | 업로드 시 저장된 이미지의 MIME 타입 (`product_review_images.mime_type`) |
| Cache-Control | `public, max-age=31536000` | 공개 이미지 장기 캐싱 (1년) |
| Content-Disposition | `attachment; filename="review.jpg"` | 업로드 시의 원본 파일명 (`original_filename`) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200 OK
Content-Type: image/jpeg
Cache-Control: public, max-age=31536000
Content-Disposition: attachment; filename="review.jpg"
<이미지 바이너리 스트림>
```
이미지를 찾지 못한 경우에만 JSON 봉투가 반환됩니다.
```json
{
"success": false,
"message": "이미지를 찾을 수 없습니다."
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 해시에 해당하는 리뷰 이미지가 없거나(`findByHash()` → null), 레코드는 있으나 스토리지에 실제 파일이 없는 경우 (`messages.reviews.image_not_found`) |
<!-- @generated:end -->
@@ -189,11 +189,38 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드. `action` 값에 따라 반환 키가 달라집니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted_count | integer | `3` | 삭제된 리뷰 건수 (`action=delete` 일 때만 반환) |
| updated_count | integer | `3` | 상태가 변경된 리뷰 건수 (`action=change_status` 일 때만 반환) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
`action=delete` (message: `messages.reviews.bulk_deleted`)
```json
{
"success": true,
"message": "선택한 리뷰가 삭제되었습니다.",
"data": {
"deleted_count": 3
}
}
```
`action=change_status` (message: `messages.reviews.bulk_updated`)
```json
{
"success": true,
"message": "선택한 리뷰의 상태가 변경되었습니다.",
"data": {
"updated_count": 3
}
}
```
**에러 응답**
@@ -201,7 +228,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`ids` 누락/빈 배열, `ids.*` 가 존재하지 않는 리뷰 ID, `action` 이 `delete`/`change_status` 외의 값, `action=change_status` 인데 `status` 누락, `status` 가 `visible`/`hidden` 외의 값) |
| 500 | Internal Server Error | 일괄 처리 중 예외 발생 (`messages.reviews.bulk_failed` — "일괄 처리에 실패했습니다.") |
<!-- @generated:end -->
@@ -502,11 +530,78 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (상태가 변경된 리뷰 리소스). 컨트롤러가 관계를 추가로 로드하지 않으므로 `product`·`images`·`image_count`·`orderOption`·`reply_admin_uuid`·`reply_admin` 등 관계 의존 키는 응답에 포함되지 않습니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 리뷰 ID (기본 키) |
| product_id | integer | `1` | 리뷰 대상 상품 ID |
| order_option_id | integer | `1` | 리뷰가 연결된 주문 옵션 ID |
| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 작성자 UUID |
| user | object | `{"uuid":"a234c2b1-...","name":"API 문서 샘플 사용자","email":"apidoc-sample-user@example.com"}` | 작성자 정보 (uuid·name·email, `user` 관계 로드 시) |
| option_snapshot | object\|null | `null` | 주문 시점 옵션 스냅샷 (옵션명 보존용, 주문 옵션에서 복사) |
| option_snapshot_label | string | `` | `option_snapshot.option_name` 의 현재 로케일 표시 문자열 |
| rating | integer | `5` | 별점 (1~5) |
| content | string | `Alias quas iusto dolorem eum eveniet …` | 리뷰 내용 |
| content_mode | string | `text` | 콘텐츠 모드: text / html |
| status | string | `hidden` | 변경된 리뷰 상태: `visible`(전시중) / `hidden`(숨김) |
| status_label | string | `숨김` | 상태의 사람이 읽는 라벨 (ReviewStatus::label()) |
| status_badge_color | string | `gray` | 상태 뱃지 색상 (visible=blue / hidden=gray) |
| has_reply | boolean | `false` | 판매자 답변 존재 여부 (`reply_content` 유무) |
| has_reply_label | string | `미답변` | `has_reply` 의 사람이 읽는 라벨 (답변완료 / 미답변) |
| has_reply_badge_color | string | `gray` | 답변 여부 뱃지 색상 (답변완료=green / 미답변=gray) |
| reply_content | string\|null | `null` | 판매자 답변 내용 (없으면 null) |
| reply_content_mode | string | `text` | 답변 콘텐츠 모드: text / html |
| replied_at | string\|null | `null` | 답변 최초 작성 일시 |
| reply_updated_at | string\|null | `null` | 답변 최종 수정 일시 |
| created_at | string | `2026-07-08 10:44:49` | 리뷰 생성 일시 |
| updated_at | string | `2026-07-08 15:10:07` | 리뷰 최종 수정 일시 (상태 변경으로 갱신됨) |
| abilities | object | `{"can_update":true,"can_delete":true}` | 현재 사용자가 이 리뷰에 수행 가능한 작업 불리언 맵 (can_update, can_delete) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "리뷰가 수정되었습니다.",
"data": {
"id": 1,
"product_id": 1,
"order_option_id": 1,
"user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"user": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자",
"email": "apidoc-sample-user@example.com"
},
"option_snapshot": null,
"option_snapshot_label": "",
"rating": 5,
"content": "Alias quas iusto dolorem eum eveniet ad omnis. Id neque consequatur fuga ut.",
"content_mode": "text",
"status": "hidden",
"status_label": "숨김",
"status_badge_color": "gray",
"has_reply": false,
"has_reply_label": "미답변",
"has_reply_badge_color": "gray",
"reply_content": null,
"reply_content_mode": "text",
"replied_at": null,
"reply_updated_at": null,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 15:10:07",
"abilities": {
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -515,7 +610,8 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | `status` 누락 또는 `visible`/`hidden` 외의 값 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 상태 변경 중 예외 발생 (`messages.reviews.update_failed` — "리뷰 수정에 실패했습니다.") |
<!-- @generated:end -->
@@ -560,11 +656,72 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (생성된 리뷰 리소스, HTTP 201). 서비스가 생성 직후의 모델을 그대로 반환하므로 `product`·`images`·`image_count`·`orderOption`·`reply_admin_uuid`·`reply_admin` 등 관계 의존 키는 응답에 포함되지 않습니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 생성된 리뷰 ID (기본 키) |
| product_id | integer | `1` | 리뷰 대상 상품 ID (요청값 그대로) |
| order_option_id | integer | `1` | 리뷰가 연결된 주문 옵션 ID (요청값 그대로) |
| user_id | string | `a234c2b1-cde8-437f-b28b-23323be2b98d` | 작성자 UUID (로그인 사용자) |
| option_snapshot | object\|null | `null` | 주문 시점 옵션 스냅샷 (주문 옵션의 `option_snapshot` 을 복사) |
| option_snapshot_label | string | `` | `option_snapshot.option_name` 의 현재 로케일 표시 문자열 |
| rating | integer | `5` | 별점 (1~5, 요청값 그대로) |
| content | string | `예시 내용입니다.` | 리뷰 내용 (10~2000자, 요청값 그대로) |
| content_mode | string | `text` | 콘텐츠 모드: text / html (미지정 시 text) |
| status | string | `visible` | 생성 시 상태 — 항상 `visible`(전시중) |
| status_label | string | `전시중` | 상태의 사람이 읽는 라벨 (ReviewStatus::label()) |
| status_badge_color | string | `blue` | 상태 뱃지 색상 (visible=blue / hidden=gray) |
| has_reply | boolean | `false` | 판매자 답변 존재 여부 (생성 직후는 항상 false) |
| has_reply_label | string | `미답변` | `has_reply` 의 사람이 읽는 라벨 (답변완료 / 미답변) |
| has_reply_badge_color | string | `gray` | 답변 여부 뱃지 색상 (답변완료=green / 미답변=gray) |
| reply_content | null | `null` | 판매자 답변 내용 (생성 직후는 null) |
| reply_content_mode | string | `text` | 답변 콘텐츠 모드: text / html |
| replied_at | null | `null` | 답변 최초 작성 일시 (생성 직후는 null) |
| reply_updated_at | null | `null` | 답변 최종 수정 일시 (생성 직후는 null) |
| created_at | string | `2026-07-08 15:20:11` | 리뷰 생성 일시 |
| updated_at | string | `2026-07-08 15:20:11` | 리뷰 최종 수정 일시 |
| abilities | object | `{"can_update":false,"can_delete":false}` | 현재 사용자가 이 리뷰에 수행 가능한 작업 불리언 맵 (관리자 권한 `sirsoft-ecommerce.reviews.update`/`.delete` 기준이므로 일반 회원은 false) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "리뷰가 작성되었습니다.",
"data": {
"id": 1,
"product_id": 1,
"order_option_id": 1,
"user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"option_snapshot": null,
"option_snapshot_label": "",
"rating": 5,
"content": "예시 내용입니다. 배송도 빠르고 품질도 만족스럽습니다.",
"content_mode": "text",
"status": "visible",
"status_label": "전시중",
"status_badge_color": "blue",
"has_reply": false,
"has_reply_label": "미답변",
"has_reply_badge_color": "gray",
"reply_content": null,
"reply_content_mode": "text",
"replied_at": null,
"reply_updated_at": null,
"created_at": "2026-07-08 15:20:11",
"updated_at": "2026-07-08 15:20:11",
"abilities": {
"can_update": false,
"can_delete": false
}
}
}
```
**에러 응답**
@@ -572,7 +729,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) 또는 작성 자격 미충족 시 `ReviewNotWritableException`(RuntimeException) 이 발생해 사유 코드가 담긴 메시지로 응답 (`order_option_not_found` / `not_own_order` / `not_confirmed` / `deadline_passed` / `already_written`) |
| 500 | Internal Server Error | 리뷰 생성 중 예외 발생 (`messages.reviews.create_failed` — "리뷰 작성에 실패했습니다.") |
<!-- @generated:end -->
@@ -694,20 +852,65 @@ Content-Type: application/octet-stream
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (업로드된 리뷰 이미지 리소스, HTTP 201)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 리뷰 이미지 ID (기본 키) |
| review_id | integer | `1` | 이미지가 속한 리뷰 ID |
| hash | string | `9f3a1c7b20de` | URL 용 고유 해시 (12자, 생성 시 자동 부여) |
| original_filename | string | `example.jpg` | 업로드된 원본 파일명 |
| download_url | string | `/api/modules/sirsoft-ecommerce/review-image/9f3a1c7b20de` | 해시 기반 이미지 서빙 URL (`review-image/{hash}`) |
| mime_type | string | `image/jpeg` | MIME 타입 (예: image/jpeg, image/webp) |
| file_size | integer | `204800` | 파일 크기 (바이트) |
| width | integer\|null | `1200` | 이미지 너비 (px, 이미지가 아니거나 판별 실패 시 null) |
| height | integer\|null | `800` | 이미지 높이 (px, 이미지가 아니거나 판별 실패 시 null) |
| alt_text | object\|null | `null` | 대체 텍스트 (다국어 JSON, 업로드 시점에는 설정하지 않으므로 null) |
| is_thumbnail | boolean | `true` | 대표 이미지 여부 (해당 리뷰의 첫 이미지면 true) |
| sort_order | integer | `1` | 정렬 순서 (기존 최대값 + 1) |
| created_at | string | `2026-07-08 15:30:02` | 업로드 일시 |
| abilities | object | `{"can_delete":false}` | 현재 사용자가 이 이미지에 수행 가능한 작업 불리언 맵 (관리자 권한 `sirsoft-ecommerce.reviews.delete` 기준이므로 일반 회원은 false) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "리뷰 이미지가 업로드되었습니다.",
"data": {
"id": 1,
"review_id": 1,
"hash": "9f3a1c7b20de",
"original_filename": "example.jpg",
"download_url": "/api/modules/sirsoft-ecommerce/review-image/9f3a1c7b20de",
"mime_type": "image/jpeg",
"file_size": 204800,
"width": 1200,
"height": 800,
"alt_text": null,
"is_thumbnail": true,
"sort_order": 1,
"created_at": "2026-07-08 15:30:02",
"abilities": {
"can_delete": false
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없는 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없거나, 대상 리뷰가 로그인 사용자의 리뷰가 아닌 경우 (`messages.reviews.forbidden` — "권한이 없습니다.") |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | `image` 파일 누락·허용 형식/용량 위반 (`error.errors` 에 필드별 메시지) 또는 리뷰당 최대 첨부 개수 초과 시 `ReviewImageUploadLimitException`(RuntimeException) — 최대 개수는 리뷰 설정 `review_settings.max_images`(기본 5) 기준 |
| 500 | Internal Server Error | 업로드 처리 중 예외 발생 (`messages.reviews.image_upload_failed` — "리뷰 이미지 업로드에 실패했습니다.") |
<!-- @generated:end -->
@@ -422,90 +422,90 @@ HTTP/1.1 200
| --- | --- | --- | --- | --- | --- |
| _tab | body | string | 아니오 | `basic_info`, `language_currency`, `seo`, `order_settings`, `claim`, `shipping`, `review_settings`, `notification_definitions`, `notifications`, `inquiry`, `mileage` | 저장할 설정 탭(카테고리) 지정 (탭별 부분 저장 식별용) |
| notifications | body | array | 아니오 | — | 알림 채널 설정 배열 (채널 ID·활성 여부·정렬 순서) |
| notifications.channels | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| notifications.channels | body | array | 아니오 | — | 알림 채널 목록. 항목별 `id`(문자열, max 50, 필수), `is_active`(boolean, 필수), `sort_order`(integer, min 0) |
| basic_info | body | array | 아니오 | — | 쇼핑몰 기본 정보 섹션 (쇼핑몰명·라우트 경로·상호·사업자번호·주소·연락처 등) |
| basic_info.shop_name | body | string | 아니오 | max 255 | basic info.shop 이름 (식별자) |
| basic_info.route_path | body | string | 아니오 | max 100 | <!-- TODO: 용도 --> |
| basic_info.no_route | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| basic_info.company_name | body | string | 아니오 | max 255 | basic info.company 이름 (식별자) |
| basic_info.business_number_1 | body | string | 아니오 | max 3 | <!-- TODO: 용도 --> |
| basic_info.business_number_2 | body | string | 아니오 | max 2 | <!-- TODO: 용도 --> |
| basic_info.business_number_3 | body | string | 아니오 | max 5 | <!-- TODO: 용도 --> |
| basic_info.ceo_name | body | string | 아니오 | max 100 | basic info.ceo 이름 (식별자) |
| basic_info.business_type | body | string | 아니오 | max 100 | <!-- TODO: 용도 --> |
| basic_info.business_category | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| basic_info.zipcode | body | string | 아니오 | max 10 | <!-- TODO: 용도 --> |
| basic_info.base_address | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| basic_info.detail_address | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| basic_info.phone_1 | body | string | 아니오 | max 4 | <!-- TODO: 용도 --> |
| basic_info.phone_2 | body | string | 아니오 | max 4 | <!-- TODO: 용도 --> |
| basic_info.phone_3 | body | string | 아니오 | max 4 | <!-- TODO: 용도 --> |
| basic_info.fax_1 | body | string | 아니오 | max 4 | <!-- TODO: 용도 --> |
| basic_info.fax_2 | body | string | 아니오 | max 4 | <!-- TODO: 용도 --> |
| basic_info.fax_3 | body | string | 아니오 | max 4 | <!-- TODO: 용도 --> |
| basic_info.email_id | body | string | 아니오 | max 100 | basic info.email 식별자 |
| basic_info.email_domain | body | string | 아니오 | max 100 | <!-- TODO: 용도 --> |
| basic_info.privacy_officer | body | string | 아니오 | max 100 | <!-- TODO: 용도 --> |
| basic_info.privacy_officer_email | body | email | 아니오 | max 255 | <!-- TODO: 용도 --> |
| basic_info.mail_order_number | body | string | 아니오 | max 100 | <!-- TODO: 용도 --> |
| basic_info.telecom_number | body | string | 아니오 | max 100 | <!-- TODO: 용도 --> |
| basic_info.shop_name | body | string | 아니오 | max 255 | 쇼핑몰 이름 (`basic_info` 전달 시 필수) |
| basic_info.route_path | body | string | 아니오 | max 100 | 쇼핑몰 라우트 경로 (예: `shop`). `no_route: true` 이면 검증에서 제외 |
| basic_info.no_route | body | boolean | 아니오 | — | 라우트 경로를 사용하지 않음 (쇼핑몰만 단독 운영하는 경우) |
| basic_info.company_name | body | string | 아니오 | max 255 | 상호(회사명) |
| basic_info.business_number_1 | body | string | 아니오 | max 3 | 사업자등록번호 앞 3자리 |
| basic_info.business_number_2 | body | string | 아니오 | max 2 | 사업자등록번호 가운데 2자리 |
| basic_info.business_number_3 | body | string | 아니오 | max 5 | 사업자등록번호 뒤 5자리 |
| basic_info.ceo_name | body | string | 아니오 | max 100 | 대표자 이름 |
| basic_info.business_type | body | string | 아니오 | max 100 | 업태 (예: 도매, 소매) |
| basic_info.business_category | body | string | 아니오 | max 255 | 종목 (예: 전자상거래) |
| basic_info.zipcode | body | string | 아니오 | max 10 | 사업장 우편번호 |
| basic_info.base_address | body | string | 아니오 | max 500 | 사업장 기본주소 |
| basic_info.detail_address | body | string | 아니오 | max 255 | 사업장 상세주소 |
| basic_info.phone_1 | body | string | 아니오 | max 4 | 대표 전화번호 첫 번째 마디 |
| basic_info.phone_2 | body | string | 아니오 | max 4 | 대표 전화번호 두 번째 마디 |
| basic_info.phone_3 | body | string | 아니오 | max 4 | 대표 전화번호 세 번째 마디 |
| basic_info.fax_1 | body | string | 아니오 | max 4 | 대표 팩스번호 첫 번째 마디 |
| basic_info.fax_2 | body | string | 아니오 | max 4 | 대표 팩스번호 두 번째 마디 |
| basic_info.fax_3 | body | string | 아니오 | max 4 | 대표 팩스번호 세 번째 마디 |
| basic_info.email_id | body | string | 아니오 | max 100 | 대표 E-mail 의 아이디 부분 (`@` 앞) |
| basic_info.email_domain | body | string | 아니오 | max 100 | 대표 E-mail 의 도메인 부분 (`@` 뒤) |
| basic_info.privacy_officer | body | string | 아니오 | max 100 | 개인정보 보호책임자 이름 |
| basic_info.privacy_officer_email | body | email | 아니오 | max 255 | 개인정보 보호책임자 E-mail |
| basic_info.mail_order_number | body | string | 아니오 | max 100 | 통신판매업 신고번호 |
| basic_info.telecom_number | body | string | 아니오 | max 100 | 부가통신 사업자번호 |
| language_currency | body | array | 아니오 | — | 통화 설정 섹션 (기본 통화·통화 목록: 코드·다국어명·환율·반올림 규칙·통화별 로케일) |
| language_currency.default_currency | body | string | 아니오 | max 10 | <!-- TODO: 용도 --> |
| language_currency.currencies | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| language_currency.default_currency | body | string | 아니오 | max 10 | 쇼핑몰 기본(base) 통화 코드. 상품/주문이 1건이라도 생성된 뒤에는 변경 불가 |
| language_currency.currencies | body | array | 아니오 | — | 등록 통화 목록. 항목별 `code`(ISO 4217 3자리 대문자, 필수)·`name`(다국어 배열, 필수)·`symbol`·`exchange_rate`·`base_unit`·`rounding_unit`·`rounding_method`(`floor`\|`round`\|`ceil`)·`decimal_places`·`is_default`·`locales` |
| seo | body | array | 아니오 | — | SEO 메타 설정 섹션 (페이지 유형별 메타 타이틀/설명·SEO 활성 토글) |
| seo.meta_category_title | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo.meta_category_description | body | string | 아니오 | max 1000 | <!-- TODO: 용도 --> |
| seo.meta_search_title | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo.meta_search_description | body | string | 아니오 | max 1000 | <!-- TODO: 용도 --> |
| seo.meta_product_title | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo.meta_product_description | body | string | 아니오 | max 1000 | <!-- TODO: 용도 --> |
| seo.meta_shop_index_title | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo.meta_shop_index_description | body | string | 아니오 | max 1000 | <!-- TODO: 용도 --> |
| seo.seo_category | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.seo_search_result | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.seo_product_detail | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.seo_shop_index | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| seo.meta_category_title | body | string | 아니오 | max 500 | 카테고리 페이지 메타 Title (`{commerce_name}`·`{category_name}` 등 변수 사용 가능) |
| seo.meta_category_description | body | string | 아니오 | max 1000 | 카테고리 페이지 메타 Description |
| seo.meta_search_title | body | string | 아니오 | max 500 | 검색 결과 페이지 메타 Title (`{keyword_name}` 변수 사용 가능) |
| seo.meta_search_description | body | string | 아니오 | max 1000 | 검색 결과 페이지 메타 Description |
| seo.meta_product_title | body | string | 아니오 | max 500 | 상품 상세 페이지 메타 Title (`{product_name}` 변수 사용 가능) |
| seo.meta_product_description | body | string | 아니오 | max 1000 | 상품 상세 페이지 메타 Description |
| seo.meta_shop_index_title | body | string | 아니오 | max 500 | 쇼핑몰 메인 페이지 메타 Title |
| seo.meta_shop_index_description | body | string | 아니오 | max 1000 | 쇼핑몰 메인 페이지 메타 Description |
| seo.seo_category | body | boolean | 아니오 | — | 카테고리 페이지에 검색엔진 친화적 페이지 제공 여부 |
| seo.seo_search_result | body | boolean | 아니오 | — | 검색결과 페이지에 검색엔진 친화적 페이지 제공 여부 |
| seo.seo_product_detail | body | boolean | 아니오 | — | 상품 상세페이지에 검색엔진 친화적 페이지 제공 여부 |
| seo.seo_shop_index | body | boolean | 아니오 | — | 쇼핑몰 메인 페이지에 검색엔진 친화적 페이지 제공 여부 |
| inquiry | body | array | 아니오 | — | 문의 연동 설정 섹션 (문의 게시판 slug) |
| inquiry.board_slug | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| inquiry.board_slug | body | string | 아니오 | max 255 | 상품 1:1 문의를 연동할 게시판 slug (게시판 모듈 활성화 시에만 선택 가능) |
| order_settings | body | array | 아니오 | — | 주문/결제 설정 섹션 (기본 PG·결제수단·은행/무통장 계좌·자동취소·장바구니 만료 등) |
| order_settings.default_pg_provider | body | string | 아니오 | max 50 | <!-- TODO: 용도 --> |
| order_settings.cash_receipt_provider | body | string | 아니오 | max 50 | <!-- TODO: 용도 --> |
| order_settings.cash_receipt_self_issue | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| order_settings.shipping_fee_tax_policy | body | string | 아니오 | `proportional`, `taxable`, `follow_main_item` | <!-- TODO: 용도 --> |
| order_settings.payment_methods | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| order_settings.banks | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| order_settings.bank_accounts | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| order_settings.auto_cancel_expired | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| order_settings.auto_cancel_days | body | integer | 아니오 | min 0, max 30 | <!-- TODO: 용도 --> |
| order_settings.cart_expiry_days | body | integer | 아니오 | min 1, max 365 | <!-- TODO: 용도 --> |
| order_settings.stock_restore_on_cancel | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| order_settings.confirmable_statuses | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| order_settings.default_pg_provider | body | string | 아니오 | max 50 | 결제 처리에 사용할 기본 PG사 ID (결제수단별 `pg_provider` 미지정 시 적용) |
| order_settings.cash_receipt_provider | body | string | 아니오 | max 50 | 현금영수증 발급 프로바이더 ID. 결제 PG 와 독립 선택하며 빈 문자열이면 미사용 |
| order_settings.cash_receipt_self_issue | body | boolean | 아니오 | — | 현금영수증 자진발급 사용 (구매자가 신청하지 않은 무통장 입금 건도 자동 발급) |
| order_settings.shipping_fee_tax_policy | body | string | 아니오 | `proportional`, `taxable`, `follow_main_item` | 현금영수증의 배송비 과세 방식 (안분 / 전액 과세 / 주된 재화 기준) |
| order_settings.payment_methods | body | array | 아니오 | — | 결제수단 목록. 항목별 `id`(필수)·`pg_provider`·`sort_order`(min 1)·`is_active`·`min_order_amount`(min 0)·`stock_deduction_timing`(`order_placed`\|`payment_complete`\|`none`)·`mileage_deduction_timing`(`order_placed`\|`payment_complete`). 최소 1개는 활성이어야 하며, PG 가 필요한 결제수단을 활성화하려면 PG사 지정 필수 |
| order_settings.banks | body | array | 아니오 | — | 무통장입금용 은행 목록. 항목별 `code`(max 10, 필수)·`name`(다국어 배열, 현재 로케일 필수) |
| order_settings.bank_accounts | body | array | 아니오 | — | 무통장 입금 계좌 목록. 항목별 `bank_code`·`account_number`·`account_holder`(모두 필수)·`is_active`·`is_default`. 계좌가 있으면 최소 1건은 사용+기본 상태여야 함 |
| order_settings.auto_cancel_expired | body | boolean | 아니오 | — | 입금대기 상태 주문의 자동취소 사용 여부 |
| order_settings.auto_cancel_days | body | integer | 아니오 | min 0, max 30 | 자동취소 기한(일). 주문일 포함 이 일수 경과 시 입금대기 주문을 자동 취소 |
| order_settings.cart_expiry_days | body | integer | 아니오 | min 1, max 365 | 장바구니 보관기간(일). 경과 시 담긴 상품 자동 삭제 |
| order_settings.stock_restore_on_cancel | body | boolean | 아니오 | — | 주문 취소 시 차감된 재고 자동 복구 여부 (반품/교환에도 적용) |
| order_settings.confirmable_statuses | body | array | 아니오 | — | 사용자가 구매확정할 수 있는 주문 옵션 상태 목록 (`payment_complete`, `shipping_hold`, `preparing`, `shipping_ready`, `shipping`, `delivered` 중 선택) |
| claim | body | array | 아니오 | — | 클레임 설정 섹션 (환불 사유 목록, DB 동기화 대상으로 분리 저장) |
| claim.refund_reasons | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| claim.refund_reasons | body | array | 아니오 | — | 환불 사유 목록. 항목별 `id`·`code`(소문자·언더스코어, 필수)·`name`(다국어 배열, 현재 로케일 필수)·`fault_type`(`customer`\|`seller`\|`carrier`, 필수)·`is_user_selectable`·`is_active`·`sort_order`. 코드 중복 불가 |
| review_settings | body | array | 아니오 | — | 리뷰 정책 섹션 (작성 기한일·이미지 최대 개수·이미지 최대 용량 MB) |
| review_settings.write_deadline_days | body | integer | 아니오 | min 1, max 365 | <!-- TODO: 용도 --> |
| review_settings.max_images | body | integer | 아니오 | min 0, max 20 | <!-- TODO: 용도 --> |
| review_settings.max_image_size_mb | body | integer | 아니오 | min 1, max 50 | <!-- TODO: 용도 --> |
| review_settings.write_deadline_days | body | integer | 아니오 | min 1, max 365 | 구매 확정 후 리뷰를 작성할 수 있는 기간(일) |
| review_settings.max_images | body | integer | 아니오 | min 0, max 20 | 리뷰 1건당 첨부 가능한 이미지 수 (0 = 이미지 첨부 불가) |
| review_settings.max_image_size_mb | body | integer | 아니오 | min 1, max 50 | 리뷰 이미지 파일 1개의 최대 용량(MB) |
| mileage | body | array | 아니오 | — | 마일리지 설정 섹션 (사용 여부·기본 적립률·적립 트리거·통화별 규칙·소멸/소멸 알림) |
| mileage.enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| mileage.default_earn_rate | body | number | 아니오 | min 0, max 100 | <!-- TODO: 용도 --> |
| mileage.earn_trigger | body | string | 아니오 | `delivered`, `confirmed` | <!-- TODO: 용도 --> |
| mileage.earn_delay_days | body | integer | 아니오 | min 0, max 365 | <!-- TODO: 용도 --> |
| mileage.currency_rules | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| mileage.expiry_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| mileage.expiry_days | body | integer | 아니오 | min 1, max 3650 | <!-- TODO: 용도 --> |
| mileage.expiry_notification_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| mileage.expiry_notification_days_before | body | integer | 아니오 | min 1, max 365 | <!-- TODO: 용도 --> |
| mileage.enabled | body | boolean | 아니오 | — | 마일리지 적립·사용 기능 사용 여부 (`mileage` 전달 시 필수) |
| mileage.default_earn_rate | body | number | 아니오 | min 0, max 100 | 기본 적립률(%). 상품 등록 시 기본값으로 사용되며, `enabled: true` 인 경우 0 초과여야 함 |
| mileage.earn_trigger | body | string | 아니오 | `delivered`, `confirmed` | 마일리지 적립 시점 (배송완료 / 구매확정) |
| mileage.earn_delay_days | body | integer | 아니오 | min 0, max 365 | 적립 시점 이후 실제 적립까지의 지연일 |
| mileage.currency_rules | body | array | 아니오 | — | 통화별 사용 규칙. 항목별 `currency_code`(ISO 4217, 필수)·`point_value`(1점당 금액, min 0.001)·`min_use_amount`·`use_unit`(min 1)·`max_use_type`(`percent`\|`fixed`)·`max_use_percent`·`max_use_value`. 첫 행은 기본 통화여야 하고, 등록 통화만 허용하며 중복 불가 |
| mileage.expiry_enabled | body | boolean | 아니오 | — | 마일리지 유효기간 사용 여부 (경과 시 미사용 마일리지 자동 소멸) |
| mileage.expiry_days | body | integer | 아니오 | min 1, max 3650 | 마일리지 유효기간(일) |
| mileage.expiry_notification_enabled | body | boolean | 아니오 | — | 소멸 예정 알림 발송 여부 |
| mileage.expiry_notification_days_before | body | integer | 아니오 | min 1, max 365 | 유효기간 만료 며칠 전에 소멸 예정 알림을 발송할지 |
| shipping | body | array | 아니오 | — | 배송 설정 섹션 (기본 국가·배송 가능 국가·무료배송·배송사(carriers)·배송유형(types) — carriers/types는 DB 동기화 대상으로 분리 저장) |
| shipping.default_country | body | string | 아니오 | max 10 | <!-- TODO: 용도 --> |
| shipping.available_countries | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| shipping.international_shipping_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| shipping.free_shipping_threshold | body | integer | 아니오 | min 0 | <!-- TODO: 용도 --> |
| shipping.free_shipping_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| shipping.address_validation_enabled | body | boolean | 아니오 | — | <!-- TODO: 용도 --> |
| shipping.address_api_provider | body | string | 아니오 | max 50 | <!-- TODO: 용도 --> |
| shipping.carriers | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| shipping.types | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| shipping.default_country | body | string | 아니오 | max 10 | 기본 배송 국가 코드. 새 배송정책 등록 시 기본으로 추가되며, `available_countries` 에 존재해야 함 |
| shipping.available_countries | body | array | 아니오 | — | 배송 가능 국가 목록. 항목별 `code`(max 10, 필수)·`name`(다국어 배열, 필수)·`is_active`. 코드 중복 불가 |
| shipping.international_shipping_enabled | body | boolean | 아니오 | — | 해외배송 기능 사용 여부 |
| shipping.free_shipping_threshold | body | integer | 아니오 | min 0 | 무료배송 기준 금액 (이 금액 이상 주문 시 무료배송) |
| shipping.free_shipping_enabled | body | boolean | 아니오 | — | 무료배송 기준금액 적용 여부 |
| shipping.address_validation_enabled | body | boolean | 아니오 | — | 주소 검증(주소찾기 API) 사용 여부 |
| shipping.address_api_provider | body | string | 아니오 | max 50 | 주소찾기 API 제공자 식별자 (기본값 `kakao`) |
| shipping.carriers | body | array | 아니오 | — | 배송사 목록(DB 동기화 대상). 항목별 `id`·`code`(소문자/숫자/하이픈, 필수)·`name`(다국어 배열, 현재 로케일 필수)·`type`(`domestic`\|`international`, 필수)·`tracking_url`(`{tracking_number}` 치환)·`is_active`·`sort_order`. 코드 중복 불가 |
| shipping.types | body | array | 아니오 | — | 배송유형 목록(DB 동기화 대상). 항목별 `id`·`code`(소문자/숫자/하이픈, 필수)·`name`(다국어 배열, 현재 로케일 필수)·`category`(`domestic`\|`international`\|`other`, 필수)·`is_active`·`sort_order`. 코드 중복 불가 |
**요청 예시**
@@ -1331,11 +1331,27 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| cleared | boolean | `true` | 이커머스 설정 캐시 + SEO 렌더 캐시 초기화 성공 여부 (성공 시 항상 `true`) |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "sirsoft-ecommerce::messages.settings.cache_clear_success",
"data": {
"cleared": true
}
}
```
**에러 응답**
@@ -1577,7 +1593,9 @@ HTTP/1.1 200
**에러 응답**
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 500 | Internal Server Error | 설정 조회 중 예외 발생 (`messages.settings.fetch_failed`) |
<!-- @generated:end -->
@@ -1686,7 +1704,9 @@ HTTP/1.1 200
**에러 응답**
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 500 | Internal Server Error | 설정 조회 중 예외 발생 (`messages.settings.fetch_failed`) |
<!-- @generated:end -->
@@ -1741,7 +1761,9 @@ HTTP/1.1 200
**에러 응답**
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 500 | Internal Server Error | 설정 조회 중 예외 발생 (`messages.settings.fetch_failed`) |
<!-- @generated:end -->
@@ -1817,7 +1839,9 @@ HTTP/1.1 200
**에러 응답**
_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 500 | Internal Server Error | 설정 조회 중 예외 발생 (`messages.settings.fetch_failed`) |
<!-- @generated:end -->
@@ -188,11 +188,58 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ShippingCarrierResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `13` | 생성된 배송사의 기본 키 (내부 식별자) |
| code | string | `apidoc` | 배송사 고유 코드 (cj, fedex 등 시스템 식별용) |
| name | object | `{"ko":"API 문서 샘플 배송사","en":"API Doc Sample Carrier"}` | 다국어 배송사명 (로케일별 값 객체) |
| localized_name | string | `API 문서 샘플 배송사` | `name` 의 현재 로케일 해석 값 (표시용 문자열) |
| type | string | `domestic` | 배송사 유형 (`domestic` 국내 / `international` 해외) |
| type_label | string | `국내` | `type` 값의 사람이 읽는 라벨 (`국내` / `국제`) |
| tracking_url | string\|null | `https://trace.cjlogistics.com/next/tracking.html?wblNo={tracking_number}` | 배송 추적 URL 템플릿 (`{tracking_number}` 치환자를 운송장 번호로 대체) |
| is_active | boolean | `true` | 활성 여부 (true 활성 / false 비활성) |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 (사용자 타임존 기준 문자열) |
| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 (사용자 타임존 기준 문자열) |
| creator | object | `{"id":1,"name":"관리자"}` | 생성자 정보 (creator 관계 로드 시에만 포함) |
| updater | object | `{"id":1,"name":"관리자"}` | 최종 수정자 정보 (updater 관계 로드 시에만 포함) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (모두 `sirsoft-ecommerce.settings.update` 권한 기준) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "배송사가 등록되었습니다.",
"data": {
"id": 13,
"code": "apidoc",
"name": {
"ko": "API 문서 샘플 배송사",
"en": "API Doc Sample Carrier"
},
"localized_name": "API 문서 샘플 배송사",
"type": "domestic",
"type_label": "국내",
"tracking_url": null,
"is_active": true,
"sort_order": 0,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 10:44:49",
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -477,11 +524,58 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ShippingCarrierResource`)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `13` | 수정된 배송사의 기본 키 (내부 식별자) |
| code | string | `apidoc` | 배송사 고유 코드 (cj, fedex 등 시스템 식별용) |
| name | object | `{"ko":"API 문서 샘플 배송사(수정)","en":"API Doc Sample Carrier"}` | 다국어 배송사명 (로케일별 값 객체) |
| localized_name | string | `API 문서 샘플 배송사(수정)` | `name` 의 현재 로케일 해석 값 (표시용 문자열) |
| type | string | `domestic` | 배송사 유형 (`domestic` 국내 / `international` 해외) |
| type_label | string | `국내` | `type` 값의 사람이 읽는 라벨 (`국내` / `국제`) |
| tracking_url | string\|null | `https://trace.cjlogistics.com/next/tracking.html?wblNo={tracking_number}` | 배송 추적 URL 템플릿 (`{tracking_number}` 치환자를 운송장 번호로 대체) |
| is_active | boolean | `true` | 활성 여부 (true 활성 / false 비활성) |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 (사용자 타임존 기준 문자열) |
| updated_at | string | `2026-07-08 15:12:07` | 최종 수정 일시 (사용자 타임존 기준 문자열) |
| creator | object | `{"id":1,"name":"관리자"}` | 생성자 정보 (creator 관계 로드 시에만 포함) |
| updater | object | `{"id":1,"name":"관리자"}` | 최종 수정자 정보 (updater 관계 로드 시에만 포함) |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (모두 `sirsoft-ecommerce.settings.update` 권한 기준) |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "배송사가 수정되었습니다.",
"data": {
"id": 13,
"code": "apidoc",
"name": {
"ko": "API 문서 샘플 배송사(수정)",
"en": "API Doc Sample Carrier"
},
"localized_name": "API 문서 샘플 배송사(수정)",
"type": "domestic",
"type_label": "국내",
"tracking_url": null,
"is_active": true,
"sort_order": 0,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 15:12:07",
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -223,11 +223,55 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ShippingPolicyResource`). 생성 성공 시 HTTP 201._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| name | object | `{"ko":"기본 배송정책","en":"Default Shipping Policy"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
| name_localized | string | `기본 배송정책` | `name` 의 현재 로케일 해석 값 |
| country_settings | array | `[]` | 국가별 배송 설정 목록 (`countrySettings` 관계가 로드된 경우에만 포함) |
| fee_summary | string | `KR: 배송비: 3,000원` | 활성 국가별 설정을 종합한 배송비 요약 텍스트 (활성 설정 없으면 빈 문자열) |
| countries_display | string | `🇰🇷` | 활성 배송 국가를 국기 이모지로 표시한 문자열 (최대 3개, 초과분은 `+N` 축약) |
| is_active | boolean | `true` | active 여부 |
| is_default | boolean | `false` | default 여부 |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "배송정책이 등록되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "기본 배송정책",
"en": "Default Shipping Policy"
},
"name_localized": "기본 배송정책",
"fee_summary": "KR: 배송비: 3,000원",
"countries_display": "🇰🇷",
"is_active": true,
"is_default": false,
"sort_order": 0,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 10:44:49",
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -236,6 +280,7 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.create`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 생성 처리 중 예외 발생 (`exceptions.operation_failed`) |
<!-- @generated:end -->
@@ -421,16 +466,35 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| deleted_count | integer | `2` | 실제로 삭제된 배송정책 건수 (`ShippingPolicyService::bulkDelete()` 반환값) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": ":count개 배송정책이 삭제되었습니다.",
"data": {
"deleted_count": 2
}
}
```
> 컨트롤러가 `messageParams` 를 넘기지 않으므로 `message` 의 `:count` 자리표시자는 치환되지 않은 채로 내려옵니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 일괄 삭제 처리 중 예외 발생 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
@@ -474,16 +538,35 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| updated_count | integer | `2` | 활성 상태가 실제로 변경된 배송정책 건수 (`ShippingPolicyService::bulkToggleActive()` 반환값) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": ":count개 배송정책의 사용여부가 변경되었습니다.",
"data": {
"updated_count": 2
}
}
```
> 컨트롤러가 `messageParams` 를 넘기지 않으므로 `message` 의 `:count` 자리표시자는 치환되지 않은 채로 내려옵니다.
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 일괄 변경 처리 중 예외 발생 (`exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
@@ -506,17 +589,17 @@ Content-Type: application/json
| endpoint | body | string | 예 | max 500 | 테스트로 호출할 외부 배송비 계산 API 엔드포인트 URL. 내부 네트워크 주소(사설 IP·루프백·`localhost`·`*.internal` 등)와 userinfo(`https://a@b/`) 위장 주소는 422 로 거부됩니다 — 이 주소는 쇼핑몰 서버가 대신 호출하므로 내부망 접근을 막기 위함(SSRF). 사내 배송비 계산 서버를 쓰려면 코어 환경설정의 `security.allow_internal_outbound_urls` 를 켜세요 |
| request_fields | body | array | 아니오 | — | 요청에 실어 보낼 필드명 목록 (후보 SSoT ShippingApiRequestField 5종) |
| config | body | array | 아니오 | — | API 호출 고급 설정 (HTTP 메서드·인증방식·필드 매핑·응답 형식/경로 등) |
| config.http_method | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| config.auth_type | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| config.auth_token | body | string | 아니오 | max 1000 | <!-- TODO: 용도 --> |
| config.auth_header_name | body | string | 아니오 | max 100 | config.auth header 이름 (식별자) |
| config.response_type | body | string | 아니오 | — | <!-- TODO: 용도 --> |
| config.response_path | body | string | 아니오 | max 200 | <!-- TODO: 용도 --> |
| config.field_map | body | array | 아니오 | — | <!-- TODO: 용도 --> |
| config.http_method | body | string | 아니오 | — | 외부 API 호출에 사용할 HTTP 메서드 (GET=query string bracket 표기, POST=JSON body. 미지정 시 POST) |
| config.auth_type | body | string | 아니오 | — | 인증 헤더 부착 방식 (none=인증 없음, bearer=`Authorization: Bearer {token}`, custom_header=커스텀 헤더명 + 값) |
| config.auth_token | body | string | 아니오 | max 1000 | 인증에 사용할 토큰/키 값 (`auth_type` 이 bearer/custom_header 일 때 헤더 값으로 전송) |
| config.auth_header_name | body | string | 아니오 | max 100 | `auth_type` 이 `custom_header` 일 때 토큰을 실어 보낼 헤더 이름 (예: `X-API-Key`. HTTP 헤더 토큰 문자만 허용) |
| config.response_type | body | string | 아니오 | — | 외부 API 응답에서 배송비를 추출하는 방식 (json=`response_path` 점표기 경로로 중첩 값 추출, text=본문에서 숫자/소수점만 추출) |
| config.response_path | body | string | 아니오 | max 200 | JSON 응답에서 배송비 값이 위치한 점표기 경로 (예: `data.shipping_fee`) |
| config.field_map | body | array | 아니오 | — | 요청 필드명 재매핑 표 (내부 필드명 → 외부 API 가 요구하는 파라미터명. 값은 영숫자/`_`/`.`/`-` 만 허용) |
| sample | body | array | 아니오 | — | 테스트 계산에 사용할 샘플 주문 데이터 (무게/금액/수량 등) |
| sample.group_total | body | number | 아니오 | min 0 | <!-- TODO: 용도 --> |
| sample.total_quantity | body | integer | 아니오 | min 0 | <!-- TODO: 용도 --> |
| sample.country_code | body | string | 아니오 | max 10 | <!-- TODO: 용도 --> |
| sample.group_total | body | number | 아니오 | min 0 | 테스트용 배송 그룹 합계 금액 (미지정 시 `10000`) |
| sample.total_quantity | body | integer | 아니오 | min 0 | 테스트용 배송 그룹 총 수량 (미지정 시 `1`) |
| sample.country_code | body | string | 아니오 | max 10 | 국가 코드 (ISO 3166-1 alpha-2. 미지정 시 `KR`) |
**요청 예시**
@@ -555,11 +638,55 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`OrderCalculationService::testApiCall()` 반환 배열). 외부 API 호출 실패도 HTTP 200 으로 내려오며 `ok=false` 로 구분합니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| ok | boolean | `true` | 테스트 호출 성공 여부 (외부 API 가 2xx 응답을 준 경우 true. 무응답/요청 실패 시 false) |
| http_ok | boolean | `true` | 외부 API 응답이 2xx 인지 여부 (응답을 받은 경우에만 포함) |
| reason | string | `no_response`, `request_failed` | 실패 사유 코드 (`ok=false` 인 경우에만 포함. no_response=응답 없음, request_failed=연결 실패/타임아웃) |
| error | string | `cURL error 28: Operation timed out` | 예외 메시지 (`reason=request_failed` 인 경우에만 포함) |
| request | object | `{"method":"POST","endpoint":"https://…","data":{…},"body":"{…}"}` | 실제로 보낸 요청 미리보기 (성공/실패 무관하게 항상 포함) |
| request.method | string | `POST` | 실제 사용된 HTTP 메서드 (`config.http_method` 대문자, 미지정 시 `POST`) |
| request.endpoint | string | `https://api.example.com/shipping/quote` | 호출한 외부 API URL |
| request.data | object | `{"country_code":"KR","group_total":10000,"total_quantity":1}` | `request_fields` + `field_map` 을 적용해 구성된 요청 파라미터 |
| request.body | string | `{\n "country_code": "KR"\n}` | `request.data` 를 pretty-print JSON 문자열로 직렬화한 요청 본문 미리보기 |
| response | object | `{"status":200,"body":"{\"fee\":3000}"}` | 외부 API 응답 (응답을 받은 경우에만 포함) |
| response.status | integer | `200` | 외부 API 응답 HTTP 상태코드 |
| response.body | string | `{"fee":3000}` | 외부 API 응답 본문 (과대 응답 방어를 위해 앞 4096자로 잘림) |
| extracted_fee | integer\|null | `3000` | 응답에서 추출된 배송비 (`response_type`/`response_path` 로 추출 실패 시 `null`) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "계산 API 테스트 호출을 완료했습니다.",
"data": {
"ok": true,
"http_ok": true,
"request": {
"method": "POST",
"endpoint": "https://api.example.com/shipping/quote",
"data": {
"country_code": "KR",
"group_total": 10000,
"total_quantity": 1
},
"body": "{\n \"country_code\": \"KR\",\n \"group_total\": 10000,\n \"total_quantity\": 1\n}"
},
"response": {
"status": 200,
"body": "{\"fee\":3000}"
},
"extracted_fee": 3000
}
}
```
**에러 응답**
@@ -597,7 +724,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — 컨트롤러가 `ResponseHelper::moduleSuccess()` 에 데이터를 넘기지 않아 `data` 는 `null`)._
**응답 예시**
@@ -607,6 +734,7 @@ Authorization: Bearer {YOUR_TOKEN}
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 400 | Bad Request | 삭제 처리 중 예외 발생 (사용 중인 정책 등 — `exceptions.operation_failed`) |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
@@ -701,11 +829,55 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`ShippingPolicyResource` — show 와 동일 구조)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `1` | 기본 키 (내부 식별자) |
| name | object | `{"ko":"기본 배송정책","en":"Default Shipping Policy"}` | 대상의 이름/명칭 (다국어 필드는 로케일별 값 객체) |
| name_localized | string | `기본 배송정책` | `name` 의 현재 로케일 해석 값 |
| country_settings | array | `[]` | 국가별 배송 설정 목록 (`countrySettings` 관계가 로드된 경우에만 포함) |
| fee_summary | string | `KR: 배송비: 3,000원` | 활성 국가별 설정을 종합한 배송비 요약 텍스트 (활성 설정 없으면 빈 문자열) |
| countries_display | string | `🇰🇷` | 활성 배송 국가를 국기 이모지로 표시한 문자열 (최대 3개, 초과분은 `+N` 축약) |
| is_active | boolean | `true` | active 여부 |
| is_default | boolean | `false` | default 여부 |
| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) |
| created_at | string | `2026-07-08 10:44:49` | 생성 일시 |
| updated_at | string | `2026-07-08 15:00:35` | 최종 수정 일시 |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "배송정책이 수정되었습니다.",
"data": {
"id": 1,
"name": {
"ko": "기본 배송정책",
"en": "Default Shipping Policy"
},
"name_localized": "기본 배송정책",
"fee_summary": "KR: 배송비: 3,000원",
"countries_display": "🇰🇷",
"is_active": true,
"is_default": false,
"sort_order": 0,
"created_at": "2026-07-08 10:44:49",
"updated_at": "2026-07-08 15:00:35",
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -715,6 +887,7 @@ Content-Type: application/json
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 500 | Internal Server Error | 수정 처리 중 예외 발생 (`exceptions.operation_failed`) |
<!-- @generated:end -->
@@ -46,11 +46,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| preferred_currency | string | `KRW` | 변경 후 저장된 회원 선호 결제 통화 코드 (ISO 4217, 등록 통화 중 하나) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "결제 통화가 변경되었습니다.",
"data": {
"preferred_currency": "KRW"
}
}
```
**에러 응답**
@@ -59,7 +75,8 @@ Content-Type: application/json
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-currency.manage`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `currency` 미전달 또는 등록 통화(is_default \| exchange_rate>0)가 아닌 코드 전달 시 |
| 500 | Internal Server Error | 통화 저장 처리 중 예외 발생 시 (`결제 통화 변경에 실패했습니다.`) |
<!-- @generated:end -->
@@ -45,10 +45,65 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `2` | 기본 키 (내부 식별자) |
| product_id | integer | `1732` | product 식별자 (연관 리소스 참조) |
| created_at | string | `2026-07-31T12:45:03.000000Z` | 생성 일시 |
| product | object | `{"id":1732,"name":{"ko":"면 손수건 3매입 #1","en":"Cotton Handk…` | <!-- TODO: 설명 --> |
| id | integer | `1` | 찜(위시리스트) 레코드 식별자 (DELETE `/wishlist/{id}` 의 `id`) |
| product_id | integer | `10` | 찜한 상품의 식별자 |
| created_at | string | `2026-07-14T09:12:33.000000Z` | 찜한 일시 (목록은 최신순 정렬) |
| product | object | `{ "id": 10, ... }` | 찜한 상품 정보 (`ProductListResource` 형식, 아래 하위 필드) |
`product` 하위 필드 (찜 목록에서는 상품의 브랜드·카테고리·라벨 관계만 로드되므로 옵션/배송정책 관련 필드는 응답에 포함되지 않습니다):
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `10` | 상품 식별자 |
| name | object | `{"ko":"상품명","en":"Product"}` | 상품명 다국어 원본 |
| name_localized | string | `상품명` | 현재 로케일로 해석된 상품명 |
| product_code | string | `P0000001` | 상품 코드 |
| sku | string \| null | `SKU-001` | 재고 관리 코드 |
| thumbnail_url | string \| null | `/storage/products/thumb.jpg` | 대표 썸네일 이미지 URL |
| list_price | number | `30000` | 정가 (기본 통화 기준 반올림) |
| list_price_formatted | string | `30,000원` | 정가 표시용 포맷 문자열 |
| selling_price | number | `24000` | 판매가 (기본 통화 기준 반올림) |
| selling_price_formatted | string | `24,000원` | 판매가 표시용 포맷 문자열 |
| discount_rate | integer | `20` | 정가 대비 할인율(%) |
| multi_currency_list_price | object | `{"KRW":30000,"USD":22.5}` | 통화별 정가 |
| multi_currency_selling_price | object | `{"KRW":24000,"USD":18}` | 통화별 판매가 |
| stock_quantity | integer | `100` | 재고 수량 |
| safe_stock_quantity | integer | `10` | 안전 재고 수량 |
| is_below_safe_stock | boolean | `false` | 재고가 안전 재고 미만인지 여부 |
| sales_status | string | `on_sale` | 판매 상태 값 (`on_sale`/`sold_out`/`stop_sale` 등) |
| sales_status_label | string | `판매중` | 판매 상태 라벨 (다국어) |
| sales_status_variant | string | `success` | 판매 상태 배지 색상 변형 |
| display_status | string | `visible` | 진열 상태 값 |
| display_status_label | string | `진열` | 진열 상태 라벨 (다국어) |
| display_status_variant | string | `success` | 진열 상태 배지 색상 변형 |
| categories | array | `[{"id":3,"name":"의류","is_primary":true}]` | 소속 카테고리 목록 (id/name/is_primary) |
| primary_category | string \| null | `의류` | 대표 카테고리명 |
| categories_with_path | array | `[{"id":3,"path":[...],"path_string":"패션 > 의류","is_primary":true}]` | 카테고리 경로(브레드크럼) 포함 목록 |
| brand_name | string \| null | `나이키` | 브랜드명 (현재 로케일) |
| shipping_policy_id | integer \| null | `1` | 배송 정책 식별자 |
| min_purchase_qty | integer | `1` | 최소 구매 수량 |
| max_purchase_qty | integer \| null | `10` | 최대 구매 수량 (없으면 `null`) |
| has_options | boolean | `true` | 옵션 보유 여부 |
| options | array | `[]` | 옵션 목록 (찜 목록에서는 옵션 관계 미로드로 빈 배열) |
| labels | array | `[{"name":"NEW","color":"#ff0000"}]` | 활성 라벨 목록 (정렬순) |
| review_count | integer | `0` | 리뷰 수 (리뷰 집계 미로드 시 `0`) |
| rating_avg | number | `0.0` | 평균 평점 (리뷰 집계 미로드 시 `0.0`) |
| created_at | string | `2026-07-01 10:00:00` | 상품 등록 일시 (사용자 타임존 기준) |
| updated_at | string | `2026-07-10 15:30:00` | 상품 수정 일시 (사용자 타임존 기준) |
| is_owner | boolean | `false` | 요청자가 상품 등록자(`created_by`)인지 여부 |
| abilities | object | `{"can_update":false,"can_delete":false}` | 상품 수정/삭제 권한 (권한 없으면 키 자체가 생략될 수 있음) |
`data.pagination` 필드:
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| current_page | integer | `1` | 현재 페이지 번호 |
| last_page | integer | `1` | 마지막 페이지 번호 |
| per_page | integer | `20` | 페이지당 항목 수 (기본 20, 최대 100) |
| total | integer | `0` | 전체 찜 항목 수 |
| from | integer \| null | `null` | 현재 페이지 첫 항목의 순번 (없으면 `null`) |
| to | integer \| null | `null` | 현재 페이지 마지막 항목의 순번 (없으면 `null`) |
| has_more_pages | boolean | `false` | 다음 페이지 존재 여부 |
**응답 예시**
@@ -198,11 +253,21 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "상품이 찜 목록에서 제거되었습니다.",
"data": null
}
```
**에러 응답**
@@ -22,12 +22,6 @@ return [
'status_changed' => 'Category status has been changed.',
'order_updated' => 'Category order has been changed.',
],
'category_images' => [
'uploaded' => 'Category image has been uploaded.',
'not_found' => 'Category image not found.',
'deleted' => 'Category image has been deleted.',
'reordered' => 'Category image order has been changed.',
],
'settings' => [
'fetch_success' => 'Settings retrieved successfully.',
'fetch_failed' => 'Failed to retrieve settings.',
@@ -90,6 +84,12 @@ return [
'thumbnail_set' => 'Thumbnail has been set.',
'not_found' => 'Product image not found.',
],
'category_images' => [
'uploaded' => 'Image has been uploaded.',
'deleted' => 'Image has been deleted.',
'reordered' => 'Image order has been changed.',
'not_found' => 'Category image not found.',
],
'brands' => [
'list_retrieved' => 'Brand list retrieved successfully.',
'retrieved' => 'Brand information retrieved successfully.',
@@ -22,12 +22,6 @@ return [
'status_changed' => '카테고리 상태가 변경되었습니다.',
'order_updated' => '카테고리 순서가 변경되었습니다.',
],
'category_images' => [
'uploaded' => '카테고리 이미지가 업로드되었습니다.',
'not_found' => '카테고리 이미지를 찾을 수 없습니다.',
'deleted' => '카테고리 이미지가 삭제되었습니다.',
'reordered' => '카테고리 이미지 순서가 변경되었습니다.',
],
'settings' => [
'fetch_success' => '설정을 조회했습니다.',
'fetch_failed' => '설정 조회에 실패했습니다.',
@@ -90,6 +84,12 @@ return [
'thumbnail_set' => '대표 이미지가 설정되었습니다.',
'not_found' => '상품 이미지를 찾을 수 없습니다.',
],
'category_images' => [
'uploaded' => '이미지가 업로드되었습니다.',
'deleted' => '이미지가 삭제되었습니다.',
'reordered' => '이미지 순서가 변경되었습니다.',
'not_found' => '카테고리 이미지를 찾을 수 없습니다.',
],
'brands' => [
'list_retrieved' => '브랜드 목록을 조회했습니다.',
'retrieved' => '브랜드 정보를 조회했습니다.',
@@ -63,11 +63,45 @@ Content-Disposition: form-data; name="temp_key"
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: FileUploader 컴포넌트 규약에 맞춰 `data.data` 객체에 업로드된 첨부 1건(`PageAttachmentResource`)이 담깁니다._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| data.id | integer | `1` | 첨부파일 ID (기본 키) |
| data.hash | string | `"a1b2c3d4e5f6"` | URL용 고유 해시 (12자, 생성 시 자동 부여). 공개 다운로드/미리보기 라우트의 식별자 |
| data.original_filename | string | `"example.pdf"` | 업로드된 원본 파일명 |
| data.mime_type | string | `"application/pdf"` | MIME 타입 (예: `image/jpeg`, `application/pdf`) |
| data.size | integer | `102400` | 파일 크기 (바이트) |
| data.collection | string | `"attachments"` | 첨부 컬렉션 그룹명 (미지정 시 `attachments`) |
| data.order | integer | `0` | 정렬 순서 (0 이상) |
| data.is_image | boolean | `false` | 이미지 파일 여부 (`mime_type` 이 `image/` 로 시작하는지) |
| data.download_url | string | `"/api/modules/sirsoft-page/pages/attachment/a1b2c3d4e5f6"` | 공개 hash 다운로드 URL |
| data.preview_url | string\|null | `null` | 공개 hash 이미지 미리보기 URL (이미지가 아니면 `null`) |
| data.created_at | string\|null | `"2026-07-14 10:30:00"` | 업로드 일시 (사용자 타임존 기준 포맷) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "첨부파일이 업로드되었습니다.",
"data": {
"data": {
"id": 1,
"hash": "a1b2c3d4e5f6",
"original_filename": "example.pdf",
"mime_type": "application/pdf",
"size": 102400,
"collection": "attachments",
"order": 0,
"is_image": false,
"download_url": "/api/modules/sirsoft-page/pages/attachment/a1b2c3d4e5f6",
"preview_url": null,
"created_at": "2026-07-14 10:30:00"
}
}
}
```
**에러 응답**
@@ -75,7 +109,8 @@ Content-Disposition: form-data; name="temp_key"
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.create`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 파일 누락/파일 아님/10MB 초과/허용되지 않은 확장자(`jpg,jpeg,png,gif,webp,pdf,zip,doc,docx,xls,xlsx,ppt,pptx,hwp,txt` 외) |
| 500 | Internal Server Error | 저장 중 예외 발생 시 `첨부파일 업로드에 실패했습니다.` (`error` 에 예외 메시지) |
<!-- @generated:end -->
@@ -114,11 +149,17 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만, `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "첨부파일 순서가 변경되었습니다.",
"data": null
}
```
**에러 응답**
@@ -126,7 +167,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `order` 누락/배열 아님/빈 배열, 각 원소의 `id`(정수) 또는 `order`(0 이상 정수) 누락 |
| 500 | Internal Server Error | 순서 갱신 중 예외 발생 시 `첨부파일 순서 변경에 실패했습니다.` (`error` 에 예외 메시지) |
<!-- @generated:end -->
@@ -156,11 +198,17 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만, `data` 는 `null`)._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```json
{
"success": true,
"message": "첨부파일이 삭제되었습니다.",
"data": null
}
```
**에러 응답**
@@ -168,7 +216,8 @@ Authorization: Bearer {YOUR_TOKEN}
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | `{id}` 에 해당하는 첨부파일이 없는 경우 (`첨부파일을 찾을 수 없습니다.`) |
| 500 | Internal Server Error | 삭제 실패 또는 삭제 중 예외 발생 시 `첨부파일 삭제에 실패했습니다.` |
<!-- @generated:end -->
+341 -35
View File
@@ -158,9 +158,9 @@ HTTP/1.1 200
| content_mode | body | string | 아니오 | `html`, `text` | 본문 편집 모드. `html` 은 리치 에디터 HTML, `text` 는 평문으로 저장·렌더링됩니다 (미지정 시 `html`) |
| published | body | boolean | 아니오 | — | 발행 여부 (발행된 항목만 필터) |
| seo_meta | body | array | 아니오 | — | SEO 메타 정보 맵. 하위 키 `title`(max 255)·`description`(max 500)·`keywords`(max 500)를 담습니다 |
| seo_meta.title | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| seo_meta.description | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo_meta.keywords | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo_meta.title | body | string | 아니오 | max 255 | SEO 메타 제목 (검색엔진/소셜 공유 표시 제목) |
| seo_meta.description | body | string | 아니오 | max 500 | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약) |
| seo_meta.keywords | body | string | 아니오 | max 500 | SEO 메타 키워드 (검색엔진 노출 키워드, 쉼표 구분) |
| temp_key | body | string | 아니오 | max 64 | 저장 전 첨부 업로드 시 발급받은 임시 키. 생성된 페이지에 임시 첨부를 귀속시키는 데 사용합니다 |
**요청 예시**
@@ -192,11 +192,77 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`PageResource`, HTTP 201)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `11` | 기본 키 (내부 식별자) |
| slug | string | `example-key` | URL 슬러그 (고유, 소문자·숫자·하이픈) |
| title | object | `{"ko":"예시 제목","en":"Example Title"}` | 페이지 제목 (다국어 JSON) |
| content | object | `{"ko":"<p>예시 내용입니다.</p>"}` | 페이지 본문 (다국어 JSON, 미입력 시 null) |
| content_mode | string | `html` | 본문 형식 (`html`, `text` — 미지정 시 `html`) |
| published | boolean | `true` | 발행 여부 (true: 발행, false: 미발행) |
| published_at | string\|null | `2026-07-08 10:51:59` | 발행 일시 (미발행 시 null) |
| seo_meta | object\|null | `{"title":"...","description":"...","keywords":"..."}` | SEO 메타 정보 (title, description, keywords) |
| current_version | integer | `1` | 현재 버전 번호 |
| creator | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"관리자"}` | 생성자 정보 (uuid/name — creator 관계 eager load) |
| updater | object\|null | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"관리자"}` | 최종 수정자 정보 (uuid/name — updater 관계 eager load) |
| attachments | array | `[]` | 페이지에 귀속된 첨부파일 목록 (`temp_key` 로 귀속된 임시 첨부 포함) |
| created_at | string | `2026-07-08 10:51:59` | 생성 일시 |
| updated_at | string | `2026-07-08 10:51:59` | 최종 수정 일시 |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자(`created_by`)인지 여부 |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 201
```
```json
{
"success": true,
"message": "페이지가 생성되었습니다.",
"data": {
"id": 11,
"slug": "example-key",
"title": {
"ko": "예시 제목",
"en": "Example Title"
},
"content": {
"ko": "<p>예시 내용입니다.</p>",
"en": "<p>Example body.</p>"
},
"content_mode": "html",
"published": true,
"published_at": "2026-07-08 10:51:59",
"seo_meta": {
"title": "예시 제목",
"description": "예시 내용입니다.",
"keywords": "예시,키워드"
},
"current_version": 1,
"creator": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자"
},
"updater": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자"
},
"attachments": [],
"created_at": "2026-07-08 10:51:59",
"updated_at": "2026-07-08 10:51:59",
"is_owner": true,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
@@ -204,7 +270,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.create`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 대표 케이스: `slug` 중복/형식 위반(소문자·숫자·하이픈만), `title` 누락 또는 기본 로케일 값 누락, `content_mode` 가 html/text 이외 |
| 500 | Internal Server Error | 생성 처리 중 예외 (`페이지 생성에 실패했습니다.`) |
<!-- @generated:end -->
@@ -243,11 +310,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| count | integer | `3` | 실제로 발행 상태가 변경된 페이지 건수 (메시지의 `:count` 에도 동일 값이 치환됨) |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "3개 페이지의 발행 상태가 변경되었습니다.",
"data": {
"count": 3
}
}
```
**에러 응답**
@@ -255,7 +338,8 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 대표 케이스: `ids` 누락/빈 배열, `ids.*` 가 존재하지 않는 페이지 ID, `published` 누락 또는 boolean 아님 |
| 500 | Internal Server Error | 일괄 처리 중 예외 (`일괄 발행 상태 변경에 실패했습니다.`) |
<!-- @generated:end -->
@@ -292,11 +376,27 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| exists | boolean | `false` | 해당 슬러그가 이미 사용 중인지 여부 (true: 중복 — 사용 불가, false: 사용 가능). `exclude_id` 를 넘기면 그 페이지는 중복 검사에서 제외 |
**응답 예시**
<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "슬러그 중복 확인이 완료되었습니다.",
"data": {
"exists": false
}
}
```
**에러 응답**
@@ -304,7 +404,7 @@ Content-Type: application/json
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.create`)이 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 대표 케이스: `slug` 누락, `exclude_id` 가 정수가 아님 |
<!-- @generated:end -->
@@ -334,7 +434,7 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만 — `data` 는 `null`)._
**응답 예시**
@@ -502,9 +602,9 @@ HTTP/1.1 200
| content_mode | body | string | 아니오 | `html`, `text` | 본문 편집 모드. `html` 은 리치 에디터 HTML, `text` 는 평문으로 저장·렌더링됩니다 (미지정 시 `html`) |
| published | body | boolean | 아니오 | — | 발행 여부 (발행된 항목만 필터) |
| seo_meta | body | array | 아니오 | — | SEO 메타 정보 맵. 하위 키 `title`(max 255)·`description`(max 500)·`keywords`(max 500)를 담습니다 |
| seo_meta.title | body | string | 아니오 | max 255 | <!-- TODO: 용도 --> |
| seo_meta.description | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo_meta.keywords | body | string | 아니오 | max 500 | <!-- TODO: 용도 --> |
| seo_meta.title | body | string | 아니오 | max 255 | SEO 메타 제목 (검색엔진/소셜 공유 표시 제목) |
| seo_meta.description | body | string | 아니오 | max 500 | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약) |
| seo_meta.keywords | body | string | 아니오 | max 500 | SEO 메타 키워드 (검색엔진 노출 키워드, 쉼표 구분) |
| temp_key | body | string | 아니오 | max 64 | 저장 전 첨부 업로드 시 발급받은 임시 키. 새로 업로드한 첨부를 이 페이지에 귀속시키는 데 사용합니다 |
**요청 예시**
@@ -536,20 +636,87 @@ Content-Type: application/json
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`PageResource` — `show` 와 동일 shape)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `10` | 기본 키 (내부 식별자) |
| slug | string | `apidoc-sample-page` | URL 슬러그 (고유) |
| title | object | `{"ko":"API 문서 샘플 페이지","en":"API Doc Sample Page"}` | 페이지 제목 (다국어 JSON) |
| content | object | `{"ko":"<p>수정된 본문.</p>"}` | 페이지 본문 (다국어 JSON) |
| content_mode | string | `html` | 본문 형식 (`html`, `text`) |
| published | boolean | `true` | 발행 여부 (true: 발행, false: 미발행) |
| published_at | string\|null | `2026-07-08 10:51:59` | 발행 일시 (미발행 시 null) |
| seo_meta | object\|null | `{"title":"...","description":"...","keywords":"..."}` | SEO 메타 정보 (title, description, keywords) |
| current_version | integer | `3` | 현재 버전 번호 (수정 시 이전 상태가 버전 이력으로 적재되며 증가) |
| creator | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"관리자"}` | 생성자 정보 (uuid/name — creator 관계 eager load) |
| updater | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"관리자"}` | 최종 수정자 정보 (uuid/name — updater 관계 eager load) |
| attachments | array | `[{"id":4,"hash":"ninrdtsmhhta","original_filename":"nesciunt.jpg", ...}]` | 페이지에 귀속된 첨부파일 목록 (`PageAttachmentResource` 배열) |
| created_at | string | `2026-07-08 10:51:59` | 생성 일시 |
| updated_at | string | `2026-07-08 15:03:15` | 최종 수정 일시 |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자(`created_by`)인지 여부 |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "페이지가 수정되었습니다.",
"data": {
"id": 10,
"slug": "apidoc-sample-page",
"title": {
"ko": "API 문서 샘플 페이지",
"en": "API Doc Sample Page"
},
"content": {
"ko": "<p>수정된 본문.</p>",
"en": "<p>Revised body.</p>"
},
"content_mode": "html",
"published": true,
"published_at": "2026-07-08 10:51:59",
"seo_meta": {
"title": "Dolorum et aut officia ipsam doloribus inventore.",
"description": "Sint ipsa impedit enim natus tempore quasi dignissimos praesentium numquam rerum tempore nam.",
"keywords": "illo,pariatur,quis,necessitatibus,aperiam"
},
"current_version": 3,
"creator": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자"
},
"updater": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자"
},
"attachments": [],
"created_at": "2026-07-08 10:51:59",
"updated_at": "2026-07-08 15:03:15",
"is_owner": true,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없거나, 스코프 밖 리소스인 경우 (`auth.scope_denied`) |
| 404 | Not Found | `{page}` ID 에 해당하는 페이지가 없는 경우 (`페이지를 찾을 수 없습니다.`) |
| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 대표 케이스: `title` 누락, `slug` 중복(자기 자신은 제외)/형식 위반, `content_mode` 가 html/text 이외 |
| 500 | Internal Server Error | 수정 처리 중 예외 (`페이지 수정에 실패했습니다.`) |
<!-- @generated:end -->
@@ -789,19 +956,63 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (`PageVersionResource` — `versions.index` 항목과 동일 shape)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `14` | 버전 레코드의 기본 키 (내부 식별자) |
| page_id | integer | `10` | 이 버전이 속한 페이지의 ID |
| version | integer | `2` | 버전 순번 (1부터 증가, 페이지 수정 시마다 채번) |
| title | object | `{"ko":"API 문서 샘플 페이지 (v2)","en":"API Doc Sample Page (v2)"}` | 그 시점의 페이지 제목 스냅샷 (다국어 JSON) |
| content | object | `{"ko":"<p>수정 버전 본문.</p>"}` | 그 시점의 페이지 본문 스냅샷 (다국어 JSON) |
| content_mode | string | `html` | 그 시점의 본문 형식 (`html`, `text` — 미지정 시 `html`) |
| seo_meta | object\|null | `null` | 그 시점의 SEO 메타 스냅샷 (title, description, keywords) |
| changes_summary | string\|null | `본문 보강` | 이 버전에서 무엇이 바뀌었는지 요약한 변경 설명 (없으면 null) |
| creator | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"관리자"}` | 이 버전을 만든 사용자 정보 (uuid/name — creator 관계 eager load) |
| created_at | string | `2026-07-08 10:51:59` | 버전 생성 일시 |
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "페이지 정보를 조회했습니다.",
"data": {
"id": 14,
"page_id": 10,
"version": 2,
"title": {
"ko": "API 문서 샘플 페이지 (v2)",
"en": "API Doc Sample Page (v2)"
},
"content": {
"ko": "<p>수정 버전 본문.</p>",
"en": "<p>Revised version body.</p>"
},
"content_mode": "html",
"seo_meta": null,
"changes_summary": "본문 보강",
"creator": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자"
},
"created_at": "2026-07-08 10:51:59"
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.read`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.read`)이 없거나, 스코프 밖 리소스인 경우 (`auth.scope_denied`) |
| 404 | Not Found | `{page}` 페이지 또는 `{versionId}` 버전이 없는 경우 (`페이지를 찾을 수 없습니다.`) |
| 500 | Internal Server Error | 조회 처리 중 예외 (`페이지 정보 조회에 실패했습니다.`) |
<!-- @generated:end -->
@@ -832,19 +1043,82 @@ Authorization: Bearer {YOUR_TOKEN}
**응답 필드** (`data` 내부)
<!-- 실측 제외: side-effectful-write — 응답 필드는 사람이 작성하세요. -->
_단건 응답: `data` 객체의 필드 (복원된 페이지의 `PageResource` — `show` 와 동일 shape)._
| 필드 | 타입 | 실측 예시값 | 용도/설명 |
| --- | --- | --- | --- |
| id | integer | `10` | 기본 키 (내부 식별자) |
| slug | string | `apidoc-sample-page` | URL 슬러그 (고유) |
| title | object | `{"ko":"API 문서 샘플 페이지 (v1)","en":"API Doc Sample Page (v1)"}` | 복원된 페이지 제목 (다국어 JSON — 대상 버전의 스냅샷) |
| content | object | `{"ko":"<p>초기 버전 본문.</p>"}` | 복원된 페이지 본문 (다국어 JSON — 대상 버전의 스냅샷) |
| content_mode | string | `html` | 본문 형식 (`html`, `text`) |
| published | boolean | `true` | 발행 여부 (true: 발행, false: 미발행) |
| published_at | string\|null | `2026-07-08 10:51:59` | 발행 일시 (미발행 시 null) |
| seo_meta | object\|null | `null` | SEO 메타 정보 (title, description, keywords — 대상 버전의 스냅샷) |
| current_version | integer | `3` | 현재 버전 번호 (복원 자체도 새 버전으로 적재되어 증가) |
| creator | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"관리자"}` | 생성자 정보 (uuid/name — creator 관계 eager load) |
| updater | object | `{"uuid":"a234c2b1-cde8-437f-b28b-23323be2b98d","name":"관리자"}` | 최종 수정자(복원 수행자) 정보 (uuid/name — updater 관계 eager load) |
| attachments | array | `[]` | 페이지에 귀속된 첨부파일 목록 (`PageAttachmentResource` 배열) |
| created_at | string | `2026-07-08 10:51:59` | 생성 일시 |
| updated_at | string | `2026-07-08 15:10:02` | 최종 수정 일시 (복원 시각) |
| is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자(`created_by`)인지 여부 |
| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 |
**응답 예시**
<!-- 실측 제외: side-effectful-write — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
```
```json
{
"success": true,
"message": "이전 버전으로 복원되었습니다.",
"data": {
"id": 10,
"slug": "apidoc-sample-page",
"title": {
"ko": "API 문서 샘플 페이지 (v1)",
"en": "API Doc Sample Page (v1)"
},
"content": {
"ko": "<p>초기 버전 본문.</p>",
"en": "<p>Initial version body.</p>"
},
"content_mode": "html",
"published": true,
"published_at": "2026-07-08 10:51:59",
"seo_meta": null,
"current_version": 3,
"creator": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자"
},
"updater": {
"uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d",
"name": "API 문서 샘플 사용자"
},
"attachments": [],
"created_at": "2026-07-08 10:51:59",
"updated_at": "2026-07-08 15:10:02",
"is_owner": true,
"abilities": {
"can_create": true,
"can_update": true,
"can_delete": true
}
}
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없는 경우 |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없거나, 스코프 밖 리소스인 경우 (`auth.scope_denied`) |
| 404 | Not Found | `{page}` 페이지 또는 `{versionId}` 버전이 없는 경우 (`페이지를 찾을 수 없습니다.`) |
| 500 | Internal Server Error | 복원 처리 중 예외 — 지정된 버전이 해당 페이지에 속하지 않는 경우 포함 (`버전 복원에 실패했습니다.`) |
<!-- @generated:end -->
@@ -874,17 +1148,33 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 JSON 봉투를 반환하지 않습니다. 성공 시 원본 파일 바이트를 그대로 내려주는 파일 스트리밍 응답(`StreamedResponse`, `Content-Disposition: attachment`)이며, 실패 시에만 JSON 오류 봉투를 반환합니다._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
Content-Type: image/jpeg
Content-Disposition: attachment; filename="nesciunt.jpg"
(파일 바이너리 스트림)
```
실패 시(첨부 미존재 · 미발행 페이지의 첨부를 권한 없이 요청 · 스토리지에 파일 없음):
```json
{
"success": false,
"message": "첨부파일을 찾을 수 없습니다.",
"errors": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 해시에 해당하는 첨부가 없거나(`첨부파일을 찾을 수 없습니다.`), 미발행 페이지의 첨부를 `sirsoft-page.pages.read` 권한 없이 요청한 경우(존재를 숨기기 위해 동일하게 404), 또는 DB 레코드는 있으나 스토리지에 실제 파일이 없는 경우(`첨부파일이 스토리지에 존재하지 않습니다.`) |
<!-- @generated:end -->
@@ -914,17 +1204,33 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생
**응답 필드** (`data` 내부)
<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. -->
_이 엔드포인트는 JSON 봉투를 반환하지 않습니다. 성공 시 이미지 바이트를 인라인으로 내려주는 스트리밍 응답(`StreamedResponse`, `Content-Disposition: inline`)이며, 실패 시에만 JSON 오류 봉투를 반환합니다._
**응답 예시**
<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. -->
```http
HTTP/1.1 200
Content-Type: image/jpeg
Content-Disposition: inline
(이미지 바이너리 스트림)
```
실패 시(첨부 미존재 · 이미지가 아닌 첨부 · 스토리지에 파일 없음):
```json
{
"success": false,
"message": "첨부파일이 스토리지에 존재하지 않습니다.",
"errors": null
}
```
**에러 응답**
| 상태코드 | 의미 | 발생 조건 |
| --- | --- | --- |
| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 |
| 404 | Not Found | 해시에 해당하는 첨부가 없는 경우(`첨부파일을 찾을 수 없습니다.`), 첨부가 이미지가 아니거나 스토리지에 실제 파일이 없는 경우(`첨부파일이 스토리지에 존재하지 않습니다.`). 미리보기는 발행/미발행과 무관하게 공개 서빙되므로 권한에 의한 404 는 없습니다 |
<!-- @generated:end -->